Skip to content

Designing Components

Use data classes when possible

Data classes auto-generate useful methods like copy, equals, and hashCode. Try to use them for all your components, ex:

data class Location(val x: Int, val y: Int, val z: Int)

Use sealed classes for data-less components

If a component should only ever be added to an entity, not set, a sealed class ensures it cannot be instantiated and nobody else can extend it, ex:

sealed class ReadyForBattle

entity.add<ReadyForBattle>()
entity.set(ReadyForBattle()) // Fails to compile

Avoid inheritance

Inheritance breaks modularity. Only use it if a library you work with already uses it.

class Animal { ... }
class Pig: Animal { ... }

entity().set(Pig())
Unclear whether this should be set as an animal or pig

If we set a Pig, then anything that reads Animal will think it's not there! Similarly, if we set Animal, then nothing will know it's actually a Pig. Finally, if our hierarchy is big enough, we can't reasonably set every possibility.

If absolutely necessary

Set the component both as a reasonable common parent class and as the exact class. For instance, in Minecraft we use the common Entity class and the specific mob class.


sealed class Alive
data class Oinks(interval: Duration)

entity {
    add<Alive>()
    set(Oinks(5.seconds))
}
Each component does one thing clearly

We can access either component without worrying about the other, or both if we like to.

Aim for immutable components

Keep component properties immutable (val) unless they are absolutely performance critical.

data class Health(var amount: Int)

val myHealth = Health(20)
entity {
    set(myHealth)
}
myHealth.amount = 0

We can change amount without anyone knowing!


data class Health(val amount: Int)

entity {
    set(Health(20))
    set(Health(0))
}

Changing health must be done through Geary, it can notify any listeners!

Extra

Don't set generic types like List<String>

It is highly discouraged to use generic types with your components, because we can't actually verify those types when getting a component. This is called type erasure. For example:

entity {
    set(listOf("strings")) // sets a list of strings as a component
    get<List<Int>>() // will succeed!
}

get<List<Int>>() succeeds because there is no way for us to know the generic type of the list during runtime. However, an error will be thrown when trying to access elements of the list which thought were integers, but are actually strings.