Entity Component System
ECS Block Diagram (Unity)
[An] Entity-component-system (ECS) is a software architectural pattern mostly used in video game development for the representation of game world objects.
Creating an Entity
All you need to do to create an entity is to implement the Attachable
interface with no implementing required.
If you are however wanting to implement this for an already existing class, you can use the WrappedAttachable<T>
class, and will unfortunately have to make extensions for data management, or access your wrapped attachable directly.
Creating a Component
A component is represented using the Any
class. This means that any object can be used as a component. Components are identified by their classes.
Storing and retrieving Data
The extensions on Attachable
help you manage the data of your entity:
fun <T : Any> Attachable.get(klass: KClass<T>): T?
inline fun <reified T : Any> Attachable.get(): T?
fun <T : Any> Attachable.set(value: T?, klass: KClass<T>): T?
inline fun <reified T : Any> Attachable.set(value: T?): T?
fun <T : Any> Attachable.getOrPut(value: T, klass: KClass<T>): T
inline fun <reified T : Any> Attachable.getOrPut(value: T): T
inline fun <reified T : Any> Attachable.data(): ReadWriteProperty<Any?, T?>
fun Attachable.components(): Set<WrappedComponent<*>>
So, let's see this in action!
class Entity : Attachable
data class IntegerComponent(val int: Int)
data class StringComponent(val string: String)
val entity = Entity()
println(entity.get<IntegerComponent>()) // null
println(entity.getOrPut(IntegerComponent(-1))) // IntegerComponent(int = -1)
println(entity.set<IntegerComponent>(null)) // null
println(entity.getOrPut<StringComponent>(StringComponent("hello, world!"))) // StringComponent(string = "hello, world!")
var stringComponent by entity.data<StringComponent>() // StringComponent(string = "hello, world!")
stringComponent = StringComponent("bye, world!")
println(entity.get<StringComponent>()?.string) // bye, world!
val otherEntity = Entity()
println(otherEntity.get<IntegerComponent>()) // null
println(otherEntity.get<StringComponent>()) // null
There's a lot to unpack here.
First up, we create an entity class implementing the Attachable
interface. This is our entity that can store components!
Then, we create two components:
- An
IntegerComponent
storing anInt
- A
StringComponent
storing aString
Now, we can perform any kind of data manipulation on this entity, for example we can:
- Get the component
- Get the component, or put it if it does not exist
- Set the component
- Obtain a delegating property to the component
Creating a system
To create a system, you will need to implement the System
interface and provide component archetypes this system filters:
object ExampleSystem : System {
override val archetypes: Sequence<KClass<out Any>> = sequenceOf(Position::class)
override suspend fun tick(tick: Int, attachable: Attachable) {
// This will not be null because of our [archetypes] filter.
val position = attachable.get<Position>()!!
println("Attachable ${attachable.identity} is at position $position!")
}
}
Now, we can start and stop ticking this system using the System#startTicking
and System#stopTicking
functions:
class ExamplePlugin : AxiPlugin() {
override suspend fun enable() {
// ...
ExampleSystem.startTicking()
}
}