-
Notifications
You must be signed in to change notification settings - Fork 20
System
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
There is a dedicated onEnable
and onDisable
method for an IntervalSystem
that can be overridden in case you need react on enabled
changes.
Also, an onInit
method can be used to add initialization logic to a system after the world's configuration is finished.
IteratingSystem
extends IntervalSystem
but in addition it requires you to specify the family of
entities which the system will iterate over. There are three possibilities to define a family:
-
all
: entity must have all the components specified -
none
: entity must not have any component specified -
any
: entity must have at least one of the components specified
Usually, your systems depend on certain other things like a SpriteBatch or Viewport. Fleks provides the possibility to use dependency injection for that to make it easier to adjust arguments of your systems later on without touching the code of the caller side. This is optional and not mandatory to use but I personally suggest to use it. Also, the examples below will use it but can of course be written without DI.
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
. Note the usage of the inject
function in the constructor which is how the DI mechanism works. Because we add an EventManager
as injectable in our World
, we can then inject it wherever we need it:
class EventManager {
// code omitted
}
class DayNightSystem(
// inject the EventManager instance that is registered below to our world
private val eventMgr: EventManager = inject()
) : 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()
}
}
}
fun main() {
val world = configureWorld {
injectables {
// add an EventManager instance as a dependency to the world
add(EventManager())
}
systems {
// here we use DI to create the system but you can of course pass the EventManager instance directly. It is up to you ;)
add(DayNightSystem())
}
}
}
There might be cases where you need multiple dependencies of the same type. In Fleks this can be solved via
named dependencies. When adding an injectable then per default, if you don't provide a name, it uses the KClass.simpleName
as an identifier. However, it is also possible to provide your own name to avoid name clashes e.g. when you need the same class but different instances. Here is an example of a system that takes two String parameters. They are registered by name HighscoreKey
and LevelKey
:
class NamedDependenciesSystem(
val hsKey: String = inject("HighscoreKey"), // will have the value hs-key
val levelKey: String = inject("LevelKey") // will have the value Level001
) : IntervalSystem() {
// ...
}
fun main() {
configureWorld {
injectables {
// add two String dependencies via a specific name
add("HighscoreKey", "hs-key")
add("LevelKey", "Level001")
}
systems {
add(NamedDependenciesSystem())
}
}
}
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
. We can do that via the family
function:
class AnimationSystem : IteratingSystem(
family { all(Position, Physic).none(Dead).any(Sprite, Animation) }
) {
override fun onTickEntity(entity: Entity) {
// update entities in here
}
}
Often, an IteratingSystem
needs access to the components of an entity. Let's see how we can access the PositionComponent
of an entity in the system above. For more details on what other functions are available, please check out the Component wiki section:
class Position : Component<Position> {
override fun type() = Position
companion object : ComponentType<Position>()
}
class AnimationSystem : IteratingSystem(
family { all(Position, Physic).none(Dead).any(Sprite, Animation) }
) {
override fun onTickEntity(entity: Entity) {
val entityPosition: Position = entity[Position]
}
}
If you need to modify the component configuration of an entity then this can be done via the Entity.configure
function. Inside configure
you get access to three special entity extension functions:
-
+=
: adds a component to an entity -
-=
: removes a component from an entity -
getOrAdd
: returns an existing component or, if it does not exist yet, then it adds it
The reason why the modification of components (=add and remove) is happening in a separate function is performance. Adding/removing components of an entity defines to which family it belongs. Checking if an entity is part of a family is a rather expensive operation. To avoid this calculation each time a component gets added/removed, we can optimize that to only do it once at the end when all modifications are done (=end of configure
block). This is similar to how the world.entity
function is working. It does this calculation also only once at the end of the entity
block when all components are added to the newly created entity.
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. In addition it adds 5 damage if it has more than 0 hitpoints:
data class Life(var hitpoints: Float) : Component<Life> {
override fun type() = Life
companion object : ComponentType<Life>()
}
data class Damage(var amount: Float) : Component<Damage> {
override fun type() = Damage
companion object : ComponentType<Damage>()
}
class Dead : Component<Dead> {
override fun type() = Dead
companion object : ComponentType<Dead>()
}
class DeathSystem : IteratingSystem(
family { all(Life).none(Dead) }
) {
override fun onTickEntity(entity: Entity) {
if (entity[Life].hitpoints <= 0f) {
// call 'configure' before changing an entity's components
entity.configure {
it += Dead()
it -= Life
}
} else {
entity.configure {
// the Damage(0f) instance only gets created, if the entity does not have a damage component yet
it.getOrAdd(Damage){ Damage(0f) }.amount += 5f
}
}
}
}
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
and compareEntityBy
functions help to create such a comparator in a concise way.
Here is an example of a RenderSystem
that sorts entities by their y-coordinate:
// 1) via the 'compareEntity' function
class RenderSystem : IteratingSystem(
family = family { all(Position, Render) },
comparator = compareEntity { entA, entB -> entA[Position].y.compareTo(entB[Position].y) }
) {
override fun onTickEntity(entity: Entity) {
// render entities: entities are sorted by their y-coordinate
}
}
// 2) via the 'compareEntityBy' function which requires the component to implement the Comparable interface
data class Position(
var x: Float,
var y: Float,
) : Component<Position>, Comparable<Position> {
override fun type() = Position
override fun compareTo(other: Position): Int {
return y.compareTo(other.y)
}
companion object : ComponentType<Position>()
}
class RenderSystem : IteratingSystem(
family = family { all(Position, Render) },
comparator = compareEntityBy(Position)
) {
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
:
class RenderSystem : IteratingSystem(
family = family { all(Position, Render) },
comparator = compareEntityBy(Position),
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 = inject(),
private val camera: Camera = inject(),
stage: Stage = inject()
) : 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 = configureWorld { /* 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 = configureWorld {}
val e1 = world.entity()
val e2 = world.entity()
val e3 = world.entity()
world -= e2
// this will iterate over entities e1 and e3
world.forEach { entity ->
// do something with the entity
}
}
You can also create families outside a system and use them in a similar way like systems:
fun main() {
val world = configureWorld {}
// Retrieve a family. If a family with such a configuration already exists, then it will be returned.
// There are no duplicate families.
val family = world.family { all(Position) }
family.forEach { entity ->
// do something with the entity
}
}