-
Notifications
You must be signed in to change notification settings - Fork 20
System (old)
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 isEachFrame
which means that it gets called every timeworld.update
is called- The other interval option is
Fixed
which 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
}
}