System (old) - Quillraven/Fleks GitHub Wiki
There are two systems in Fleks:
-
IntervalSystem: system without relation to entities -
IteratingSystem: system with relation to entities of a specific component configuration
IntervalSystem has two optional arguments:
-
interval: defines the time in milliseconds when the system should be updated. Default isEachFramewhich means that it gets called every timeworld.updateis called- The other interval option is
Fixedwhich takes a step time in milliseconds and runs the system with that fixed time step
- The other interval option is
-
enabled: defines if the system will be processed or not. Default value is true
IteratingSystem extends IntervalSystem but in addition it requires you to specify the relevant components of
entities which the system will iterate over. There are three types to define this component configuration:
-
AllOf: entity must have all the components specified -
NoneOf: entity must not have any component specified -
AnyOf: entity must have at least one of the components specified
Usually, your systems depend on certain other things like a SpriteBatch or Viewport. Fleks uses dependency injection for that to make it easier to adjust arguments of your systems later on without touching the code of the caller side.
First, let's have a look on how to create a simple IntervalSystem that gets called every time world.update is
called. It is a made up example of a Day-Night-Cycle system which switches between day and night every second and
dispatches a game event via an EventManager.
class DayNightSystem(
private val eventMgr: EventManager
) : IntervalSystem() {
private var currentTime = 0f
private var isDay = false
override fun onTick() {
// deltaTime is not needed in every system that's why it is not a parameter of "onTick".
// However, if you need it, you can still access it via the IteratingSystem's deltaTime property
currentTime += deltaTime
if (currentTime >= 1000 && !isDay) {
isDay = true
eventMgr.publishDayEvent()
} else if (currentTime >= 2000 && isDay) {
isDay = false
currentTime = 0f
eventMgr.publishNightEvent()
}
}
}
class DayNightSystem : IntervalSystem() {
private var currentTime = 0f
private var isDay = false
private val eventMgr: EventManager = Inject.dependency()
override fun onTick() {
// deltaTime is not needed in every system that's why it is not a parameter of "onTick".
// However, if you need it, you can still access it via the IteratingSystem's deltaTime property
currentTime += deltaTime
if (currentTime >= 1000 && !isDay) {
isDay = true
eventMgr.publishDayEvent()
} else if (currentTime >= 2000 && isDay) {
isDay = false
currentTime = 0f
eventMgr.publishNightEvent()
}
}
}The DayNightSystem requires an EventManager which we need to inject. To achieve that we can define it when creating
our world by using the injectables function:
val eventManager = EventManager()
val world = world {
injectables {
add(eventManager)
}
systems {
add<DayNightSystem>()
}
}
val eventManager = EventManager()
val world = world {
injectables {
add(eventManager)
}
systems {
add(::DayNightSystem)
}
}There might be cases where you need multiple dependencies of the same type. In Fleks this can be solved via
named dependencies using the Qualifier annotation. Here is an example of a system that takes two String parameters.
They are registered by name HighscoreKey and LevelKey:
private class NamedDependenciesSystem(
@Qualifier("HighscoreKey") val hsKey: String, // will have the value hs-key
@Qualifier("LevelKey") val levelKey: String // will have the value Level001
) : IntervalSystem() {
// ...
}
fun main() {
val world = world {
systems {
add<NamedDependenciesSystem>()
}
injectables {
// inject String dependencies from above via their type names
add("HighscoreKey", "hs-key")
add("LevelKey", "Level001")
}
}
}
private class NamedDependenciesSystem : IntervalSystem() {
private val hsKey: String = Inject.dependency("HighscoreKey") // will have the value "hs-key"
private val levelKey: String = Inject.dependency("LevelKey") // will have the value "Level001"
// ...
}
fun main() {
val world = world {
systems {
add(::NamedDependenciesSystem)
}
injectables {
// inject String dependencies from above via their type names
add("HighscoreKey", "hs-key")
add("LevelKey", "Level001")
}
}
}Let's create an IteratingSystem that iterates over all entities with a PositionComponent, PhysicComponent
and at least a SpriteComponent or AnimationComponent but without a DeadComponent:
@AllOf([Position::class, Physic::class])
@NoneOf([Dead::class])
@AnyOf([Sprite::class, Animation::class])
class AnimationSystem : IteratingSystem() {
override fun onTickEntity(entity: Entity) {
// update entities in here
}
}
class AnimationSystem : IteratingSystem(
allOfComponents = arrayOf(Position::class, Physic::class),
noneOfComponents = arrayOf(Dead::class),
anyOfComponents = arrayOf(Sprite::class, Animation::class)
) {
override fun onTickEntity(entity: Entity) {
// update entities in here
}
}Often, an IteratingSystem needs access to the components of an entity. In Fleks this is done via so
called ComponentMapper. ComponentMapper are automatically injected into a system and do not need to be defined in
the world's configuration.
Note that for the KMP version you need to register the components that your world requires because it needs to know how to create a component (=factory method). In the JVM version this is not needed because it uses reflection to identify the no-args constructor of a component which is not possible in KMP.
Let's see how we can access the PositionComponent of an entity in the system above:
@AllOf([Position::class, Physic::class])
@NoneOf([Dead::class])
@AnyOf([Sprite::class, Animation::class])
class AnimationSystem(
private val positions: ComponentMapper<Position>
) : IteratingSystem() {
override fun onTickEntity(entity: Entity) {
val entityPosition: Position = positions[entity]
}
}
class AnimationSystem : IteratingSystem(
allOfComponents = arrayOf(Position::class, Physic::class),
noneOfComponents = arrayOf(Dead::class),
anyOfComponents = arrayOf(Sprite::class, Animation::class)
) {
private val positions: ComponentMapper<Position> = Inject.componentMapper()
override fun onTickEntity(entity: Entity) {
val entityPosition: Position = positions[entity]
}
}
fun main() {
val world = world {
components {
// this is necessary in order to create and inject the ComponentMapper above
add(::Position)
}
}
}There is also a getOrNull version available in case a component is not mandatory
for every entity that gets processed by a system. An example is:
@AllOf([Position::class, Physic::class])
@NoneOf([Dead::class])
@AnyOf([Sprite::class, Animation::class])
class AnimationSystem(
private val animations: ComponentMapper<Animation>
) : IteratingSystem() {
override fun onTickEntity(entity: Entity) {
animations.getOrNull(entity)?.let { animation ->
// entity has animation component which can be modified inside this block
}
}
}If you need to modify the component configuration of an entity then this can be done via the configureEntity function
of an IteratingSystem. The purpose of this function is performance reasons to trigger internal calculations
of Fleks only once instead of each time a component gets added or removed. Inside configureEntity you get access to
three special ComponentMapper functions:
-
add: adds a component to an entity -
remove: removes a component from an entity -
addOrUpdate: adds a component only if it does not exist yet. Otherwise, it just updates the existing component
Let's see how a system can look like that adds a DeadComponent to an entity and removes a LifeComponent when its
hitpoints are <= 0:
@AllOf([Life::class])
@NoneOf([Dead::class])
class DeathSystem(
private val lives: ComponentMapper<Life>,
private val deads: ComponentMapper<Dead>
) : IteratingSystem() {
override fun onTickEntity(entity: Entity) {
if (lives[entity].hitpoints <= 0f) {
configureEntity(entity) {
deads.add(it)
lives.remove(it)
}
}
}
}ComponentMapper are not restricted to systems. You can get a mapper also from your world whenever needed.
Here is an example that gets the LifeComponent mapper of the snippet above:
fun main() {
val world = world { /* config omitted */ }
val lives = world.mapper<Life>()
}Sometimes it might be necessary to sort entities before iterating over them like e.g. in a RenderSystem that needs to
render entities by their y or z-coordinate. In Fleks this can be achieved by passing an EntityComparator to
an IteratingSystem. Entities are then sorted automatically every time the system gets updated. The compareEntity
function helps to create such a comparator in a concise way.
Here is an example of a RenderSystem that sorts entities by their y-coordinate:
@AllOf([Position::class, Render::class])
class RenderSystem(
private val positions: ComponentMapper<Position>
) : IteratingSystem(compareEntity { entA, entB -> positions[entA].y.compareTo(positions[entB].y) }) {
override fun onTickEntity(entity: Entity) {
// render entities: entities are sorted by their y-coordinate
}
}
class RenderSystem : IteratingSystem(
allOfComponents = arrayOf(Position::class, Render::class),
comparator = compareEntity { entA, entB -> positions[entA].y.compareTo(positions[entB].y) }
) {
private val positions: ComponentMapper<Position> = Inject.componentMapper()
override fun onTickEntity(entity: Entity) {
// render entities: entities are sorted by their y-coordinate
}
}The default SortingType is Automatic which means that the IteratingSystem is sorting automatically each time it
gets updated. This can be changed to Manual by setting the sortingType parameter accordingly. In that case
the doSort flag of the IteratingSystem
needs to be set programmatically whenever sorting should be done. The flag gets cleared after the sorting.
This is how the example above could be written with a Manual SortingType:
@AllOf([Position::class, Render::class])
class RenderSystem(
private val positions: ComponentMapper<Position>
) : IteratingSystem(
compareEntity { entA, entB -> positions[entA].y.compareTo(positions[entB].y) },
sortingType = Manual
) {
override fun onTick() {
doSort = true
super.onTick()
}
override fun onTickEntity(entity: Entity) {
// render entities: entities are sorted by their y-coordinate
}
}Sometimes a system might allocate special resources that you want to free before closing your application. An example would be a LibGDX game where a system might create a disposable resource internally.
For this purpose the world's dispose function can be used which first removes all
entities of the world and afterwards calls the onDispose function of each system.
Here is an example of a DebugSystem that creates a Box2D
debug renderer for the physics internally and disposes it:
class DebugSystem(
private val box2dWorld: World,
private val camera: Camera,
stage: Stage
) : IntervalSystem() {
private val renderer = Box2DDebugRenderer()
override fun onTick() {
physicRenderer.render(box2dWorld, camera.combined)
}
// this is an optional function that can be used to free specific resources
override fun onDispose() {
renderer.dispose()
}
}
fun main() {
val world = world { /* configuration omitted */ }
// following call disposes the DebugSystem
world.dispose()
}
class DebugSystem : IntervalSystem() {
private val box2dWorld: World = Inject.dependency()
private val camera: Camera = Inject.dependency()
private val stage: Stage = Inject.dependency()
private val renderer = Box2DDebugRenderer()
override fun onTick() {
physicRenderer.render(box2dWorld, camera.combined)
}
// this is an optional function that can be used to free specific resources
override fun onDispose() {
renderer.dispose()
}
}
fun main() {
val world = world { /* configuration omitted */ }
// following call disposes the DebugSystem
world.dispose()
}If you ever need to iterate over entities outside a system then this is also possible but please note that
systems are always the preferred way of iteration in an entity component system.
The world's forEach function allows you to iterate over all active entities:
fun main() {
val world = world {}
val e1 = world.entity()
val e2 = world.entity()
val e3 = world.entity()
world.remove(e2)
// this will iterate over entities e1 and e3
world.forEach { entity ->
// do something with the entity
}
}