Skip to content

Commit

Permalink
#123: aggregated family hooks that trigger IntervalSystems (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
metaphore authored Dec 1, 2023
1 parent 98f9428 commit 187addd
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,11 @@ class FleksWrongConfigurationUsageException :
"The global functions 'inject' and 'family' must be used inside a WorldConfiguration scope." +
"The same applies for 'compareEntityBy' and 'compareEntity' unless you specify the world parameter explicitly."
)

class FleksWrongSystemInterfaceException(system: KClass<*>, `interface`: KClass<*>) :
FleksException("System ${system.simpleName} cannot have interface ${`interface`.simpleName}")

class FleksWorldModificationDuringConfigurationException :
FleksException("Entities were added during world configuration. " +
"Most likely in a constructor of a system. " +
"Create those entities in the 'onInit' method of a system instead.")
27 changes: 27 additions & 0 deletions src/commonMain/kotlin/com/github/quillraven/fleks/system.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ abstract class IntervalSystem(
val deltaTime: Float
get() = if (interval is Fixed) interval.step else world.deltaTime

/**
* This function gets called when the [world configuration][WorldConfiguration.configure] is completed.
*/
open fun onInit() = Unit

/**
* This function gets called whenever the system gets [enabled].
*/
Expand Down Expand Up @@ -204,3 +209,25 @@ abstract class IteratingSystem(
private val EMPTY_COMPARATOR = EntityComparator { _, _ -> 0 }
}
}

/**
* Any [IteratingSystem] having this interface will be triggered
* by own [Family] similarly to [Family.addHook].
*/
interface FamilyOnAdd {
/**
* Gets called whenever an [entity][Entity] enters the family.
*/
fun onAddEntity(entity: Entity)
}

/**
* Any [IteratingSystem] having this interface will be triggered
* by own [Family] similarly to [Family.removeHook].
*/
interface FamilyOnRemove {
/**
* Gets called whenever an [entity][Entity] leaves the family.
*/
fun onRemoveEntity(entity: Entity)
}
62 changes: 62 additions & 0 deletions src/commonMain/kotlin/com/github/quillraven/fleks/world.kt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,68 @@ class WorldConfiguration(@PublishedApi internal val world: World) {
// instead of resizing every time a system gets added to the configuration
world.systems = it.systems.toTypedArray()
}

if (world.numEntities > 0) {
throw FleksWorldModificationDuringConfigurationException()
}

setUpAggregatedFamilyHooks()

world.systems.forEach { it.onInit() }
}

/**
* Extend [Family.addHook] and [Family.removeHook] with
* other objects that needed to triggered by the hooks.
*/
private fun setUpAggregatedFamilyHooks() {

// validate systems against illegal interfaces
world.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
world.systems
.mapNotNull { if (it is IteratingSystem && it is FamilyOnAdd) it else null }
.groupBy { it.family }
.forEach { entry ->
val (family, systemList) = entry
val ownHook = family.addHook
val systemArray = systemList.toTypedArray()
family.addHook = if (ownHook != null) { entity ->
ownHook(world, entity)
systemArray.forEach { it.onAddEntity(entity) }
} else { entity ->
systemArray.forEach { it.onAddEntity(entity) }
}
}

// register family hooks for IteratingSystem.FamilyOnRemove containing systems
world.systems
.mapNotNull { if (it is IteratingSystem && it is FamilyOnRemove) it else null }
.groupBy { it.family }
.forEach { entry ->
val (family, systemList) = entry
val ownHook = family.removeHook
val systemArray = systemList.toTypedArray()
family.removeHook = if (ownHook != null) { entity ->
ownHook(world, entity)
systemArray.forEach { it.onRemoveEntity(entity) }
} else { entity ->
systemArray.forEach { it.onRemoveEntity(entity) }
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.github.quillraven.fleks

import com.github.quillraven.fleks.World.Companion.family
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue

private class SimpleTestComponent : Component<SimpleTestComponent> {
companion object : ComponentType<SimpleTestComponent>()
override fun type() = SimpleTestComponent
}

private class OnAddHookSystem : IteratingSystem(
family = family { all(SimpleTestComponent) }
), FamilyOnAdd {

var addEntityHandled = false

override fun onAddEntity(entity: Entity) {
addEntityHandled = true
}

override fun onTickEntity(entity: Entity) = Unit
}

private class OnRemoveHookSystem : IteratingSystem(
family = family { all(SimpleTestComponent) }
), FamilyOnRemove {

var removeEntityHandled = false

override fun onRemoveEntity(entity: Entity) {
removeEntityHandled = true
}

override fun onTickEntity(entity: Entity) = Unit
}

private class IllegalOnAddHookSystem : IntervalSystem(), FamilyOnAdd {
override fun onTick() = Unit
override fun onAddEntity(entity: Entity) = Unit
}

private class IllegalOnRemoveHookSystem : IntervalSystem(), FamilyOnRemove {
override fun onTick() = Unit
override fun onRemoveEntity(entity: Entity) = Unit
}

internal class FamilySystemHookTest {

@Test
fun onAddHookSystem() {
val world = configureWorld {
systems {
add(OnAddHookSystem())
}
}

world.entity { it += SimpleTestComponent() }

val system = world.system<OnAddHookSystem>()
assertTrue { system.addEntityHandled }
}

@Test
fun onRemoveHookSystem() {
val world = configureWorld {
systems {
add(OnRemoveHookSystem())
}
}

val entity = world.entity { it += SimpleTestComponent() }
world -= entity

val system = world.system<OnRemoveHookSystem>()
assertTrue { system.removeEntityHandled }
}

@Test
fun illegalOnAddHookSystem() {
assertFailsWith<FleksWrongSystemInterfaceException> {
configureWorld {
systems {
add(IllegalOnAddHookSystem())
}
}
}
}

@Test
fun illegalOnRemoveHookSystem() {
assertFailsWith<FleksWrongSystemInterfaceException> {
configureWorld {
systems {
add(IllegalOnRemoveHookSystem())
}
}
}
}
}
39 changes: 38 additions & 1 deletion src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ import kotlin.test.*
private class SystemTestIntervalSystemEachFrame : IntervalSystem(
interval = EachFrame
) {
var numInits = 0
var numDisposes = 0
var numCalls = 0

override fun onInit() {
numInits++
}

override fun onTick() {
++numCalls
}
Expand Down Expand Up @@ -73,7 +78,8 @@ private class SystemTestIteratingSystem : IteratingSystem(
private class SystemTestEntityCreation : IteratingSystem(family { any(SystemTestComponent) }) {
var numTicks = 0

init {
override fun onInit() {
super.onInit()
world.entity { it += SystemTestComponent() }
}

Expand Down Expand Up @@ -171,6 +177,15 @@ private class SystemTestEnable(enabled: Boolean) : IntervalSystem(enabled = enab
override fun onTick() = Unit
}

private class AddEntityInConstructorSystem() : IntervalSystem() {

init {
world.entity { }
}

override fun onTick() = Unit
}

internal class SystemTest {
@Test
fun systemWithIntervalEachFrameGetsCalledEveryTime() {
Expand Down Expand Up @@ -418,6 +433,17 @@ internal class SystemTest {
assertEquals(4, system.numEntityCalls)
}

@Test
fun initService() {
val world = configureWorld {
systems {
add(SystemTestIntervalSystemEachFrame())
}
}

assertEquals(1, world.system<SystemTestIntervalSystemEachFrame>().numInits)
}

@Test
fun disposeService() {
val world = configureWorld {
Expand Down Expand Up @@ -483,4 +509,15 @@ internal class SystemTest {
assertFalse(system.enabledCall)
assertFalse(system.disabledCall)
}

@Test
fun testWorldModificationDuringConfiguration() {
assertFailsWith<FleksWorldModificationDuringConfigurationException> {
configureWorld {
systems {
add(AddEntityInConstructorSystem())
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ private class WorldTestIteratingSystem(
}

private class WorldTestInitSystem : IteratingSystem(family { all(WorldTestComponent) }) {
init {
override fun onInit() {
super.onInit()
world.entity { it += WorldTestComponent() }
}

Expand All @@ -76,7 +77,8 @@ private class WorldTestInitSystem : IteratingSystem(family { all(WorldTestCompon
private class WorldTestInitSystemExtraFamily : IteratingSystem(family { all(WorldTestComponent) }) {
val extraFamily = world.family { any(WorldTestComponent2).none(WorldTestComponent) }

init {
override fun onInit() {
super.onInit()
world.entity { it += WorldTestComponent2() }
}

Expand Down

0 comments on commit 187addd

Please sign in to comment.