Fleks 2.0 #66
Replies: 7 comments 26 replies
-
Just fiddled around some more. Very similar to above but now a ComponentType has a unique number and therefore, we could almost use the same structure as Fleks already has. Would be great if somehow it's possible to avoid defining the companion object and the override val type per component. Companion object would be possible via an annotation processor but override val type isn't from what I know. UPDATE: actually type must be a function because I just checked the decompiled code and looks like with a val it is creating a final field for each component which of course we don't want. We just want a static type/id per component. So, I updated it now to a function and this seems to be having no impact on the speed (it actually is faster for some reason, maybe because the class creation is easier because of one less final property per instance). import kotlin.system.measureTimeMillis
open class ComponentType<C>(val id: Int = nextId++) {
companion object {
private var nextId = 0
}
}
interface Component<C> {
fun type(): ComponentType<C>
fun onRemove(entity: Entity) = Unit
}
data class Position(var x: Float = 0f, var y: Float = 0f) : Component<Position> {
companion object : ComponentType<Position>()
override fun type() = Position
}
data class Graphic(var texture: String = "tex") : Component<Graphic> {
companion object : ComponentType<Graphic>()
override fun type() = Graphic
}
class Entity {
val components = bag<Component<*>>()
operator fun plusAssign(component: Component<*>) {
components[component.type().id] = component
}
inline operator fun <reified C> get(type: ComponentType<C>): C {
return components[type.id] as C
}
}
class World {
val living = mutableSetOf<Entity>()
private val recycled = ArrayDeque<Entity>()
fun entity(cfg: (Entity) -> Unit = {}): Entity {
val entity = if (recycled.isEmpty()) {
Entity().apply(cfg)
} else {
recycled.removeLast().apply(cfg)
}
living += entity
return entity
}
operator fun minusAssign(entity: Entity) {
entity.components.forEach { it.onRemove(entity) }
recycled += entity
living -= entity
}
}
fun main() {
val world = World()
val times = mutableListOf<Long>()
repeat(5) {
times += measureTimeMillis {
repeat(10_000) {
world.entity { entity ->
entity += Position()
}
}
}
val entities = world.living.toTypedArray()
times[it] += measureTimeMillis {
entities.forEach { entity ->
world -= entity
}
}
}
println("MAX: ${times.max()} | MIN: ${times.min()} | AVG: ${times.average()}")
val player = world.entity {
it += Position()
it += Graphic()
}
println(player[Position])
println(player[Graphic].texture)
world -= player
} |
Beta Was this translation helpful? Give feedback.
-
Aah, this is exactly what I was missing. Now, it makes much more sense :) I like the idea that we do not change too much in Fleks. Removing the need for the factory methods when creating components and (family) listeners could be the first step. If you remove other reflection code, keep in mind that some reflect features are available in KMP. Also I would therefore only remove it if the alternative implementation would bring some more benefit e.g. faster execution.
I also like this idea very much :) |
Beta Was this translation helpful? Give feedback.
-
I tried something very quickly now by only adjusting the necessary code to run the Fleks Benchmarks with the new idea and indeed it gets faster. What is interesting as well, ony my PC I only rarely get an OutOfMemory exception for the Artemis "addRemove" benchmark. On my notebook it is 100% failing. Seems like something is messed up in Artemis a little bit??? Anyway, here is the result for Fleks:
So looks like this will improve performance by ~10-20% which is not bad in my opinion. Two things came to my attention when adjusting the code:
edit: oh and by the way to lower the expectations. I have no plans to release a 2.0 version in the upcoming weeks. I am not even sure if I can do it within this year. This is more likely something for next year because of time constraints due to things that are happening in my personal life (mainly positive things :) ). |
Beta Was this translation helpful? Give feedback.
-
I forgot to add the code. This would be the same API for JVM and KMP. No more reflection necessary so far. I replaced the family annotations of a system with a new package com.github.quillraven.fleks.benchmark
import com.github.quillraven.fleks.*
import org.openjdk.jmh.annotations.*
import java.util.concurrent.TimeUnit
data class FleksPosition(var x: Float = 0f, var y: Float = 0f) : Component<FleksPosition> {
companion object : ComponentType<FleksPosition>()
override fun type() = FleksPosition
}
data class FleksLife(var life: Float = 0f) : Component<FleksLife> {
companion object : ComponentType<FleksLife>()
override fun type() = FleksLife
}
data class FleksSprite(var path: String = "", var animationTime: Float = 0f) : Component<FleksSprite> {
companion object : ComponentType<FleksSprite>()
override fun type() = FleksSprite
}
class FleksSystemSimple : IteratingSystem() {
override fun familyCfg(): FamilyCfg = FamilyCfg {
allOf(FleksPosition)
}
override fun onTickEntity(entity: Entity) {
entity[FleksPosition].x++
}
}
class FleksSystemComplex1 : IteratingSystem() {
private var actionCalls = 0
override fun familyCfg(): FamilyCfg = FamilyCfg {
allOf(FleksPosition)
noneOf(FleksLife)
anyOf(FleksSprite)
}
override fun onTickEntity(entity: Entity) {
if (actionCalls % 2 == 0) {
entity[FleksPosition].x++
configureEntity(entity) {
it += FleksLife()
}
} else {
configureEntity(entity) {
it -= FleksPosition
}
}
entity[FleksSprite].animationTime++
++actionCalls
}
}
class FleksSystemComplex2 : IteratingSystem() {
override fun familyCfg(): FamilyCfg = FamilyCfg {
anyOf(FleksPosition, FleksLife, FleksSprite)
}
override fun onTickEntity(entity: Entity) {
configureEntity(entity) {
it -= FleksLife
it += FleksPosition()
}
}
}
@State(Scope.Benchmark)
open class FleksStateAddRemove {
lateinit var world: World
@Setup(value = Level.Iteration)
fun setup() {
world = world {
entityCapacity = NUM_ENTITIES
}
}
}
@State(Scope.Benchmark)
open class FleksStateSimple {
lateinit var world: World
@Setup(value = Level.Iteration)
fun setup() {
world = world {
entityCapacity = NUM_ENTITIES
systems {
add<FleksSystemSimple>()
}
}
repeat(NUM_ENTITIES) {
world.entity {
it += FleksPosition()
}
}
}
}
@State(Scope.Benchmark)
open class FleksStateComplex {
lateinit var world: World
@Setup(value = Level.Iteration)
fun setup() {
world = world {
entityCapacity = NUM_ENTITIES
systems {
add<FleksSystemComplex1>()
add<FleksSystemComplex2>()
}
}
repeat(NUM_ENTITIES) {
world.entity {
it += FleksPosition()
it += FleksSprite()
}
}
}
}
@Fork(1)
@Warmup(iterations = WARMUPS)
@Measurement(iterations = ITERATIONS, time = TIME, timeUnit = TimeUnit.SECONDS)
open class FleksBenchmark {
@Benchmark
fun addRemove(state: FleksStateAddRemove) {
repeat(NUM_ENTITIES) {
state.world.entity {
it += FleksPosition()
}
}
repeat(NUM_ENTITIES) {
state.world.remove(Entity(it))
}
}
@Benchmark
fun simple(state: FleksStateSimple) {
repeat(WORLD_UPDATES) {
state.world.update(1f)
}
}
@Benchmark
fun complex(state: FleksStateComplex) {
repeat(WORLD_UPDATES) {
state.world.update(1f)
}
}
} |
Beta Was this translation helpful? Give feedback.
-
Hey guys, Sorry for my absence from discussions for the new version of Fleks, but professional and family situations kept me away from open-source projects for a while. I see that v2 is already well advanced and I'm really enjoying the direction the project is taking. If you still have room for collaboration, I can help by reviewing the documentation. I want to clone my game to the new version, so I can collaborate more effectively on documentation, code examples, game examples, etc. But first I need help: |
Beta Was this translation helpful? Give feedback.
-
@mariorez / @jobe-m: Let me know if some of your examples are converted to 2.0 or if you maybe have a new repository for 2.0 games. Then I will update the 2.0 section accordingly in the wiki :) |
Beta Was this translation helpful? Give feedback.
-
I see there's a |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I'd like to discuss the future of Fleks. In my opinion with version 1.6, which will be released soon, we will have a stable and nice ECS solution for JVM and KMP. I am personally very happy how everything turned out and I am actually using Fleks over Ashley in my latest projects.
However, there are a few things that I dislike or at least I am not 100% happy with it:
ComponentMapper
are handled. It is redundant to inject them into every system. While it is nice to see if a system gets too many mappers injected and therefore will alert users to maybe rethink or refactor their system, it is a little bit annoying to always define the mapper in case you need a component of a specific type.Initially I though that annotiation processing (KSP) can help with some of the problems but I read a couple of articles and now I am not sure anymore. I think it actually does not help at all :D
The reason is that you cannot modify existing source code like e.g. Lombok is doing it. You can only add additional code which means extensions, which is basically useless because Fleks does not know and cannot access companion object code of classes outside of Fleks (like all components or systems that are created by users).
My initial idea was to automatically create an ID for each component to somehow get rid of the ComponentMapper but imo this is not possible. Also, the way Kotlin works with static properties, this will never be possible the way I thought about it.
I had something in mind like:
However, from what I read, this is impossible in Kotlin and will never work. It already fails when trying to access the id of a reified property. Additionally, overriding the static id or shadowing it is not possible in Kotlin. Something like that would work in C++ but we don't use that sorcery ;)
Additionally, KSP is also in an alpha (or beta?) stage, which I also do not like a lot and it also requires more gradle setup for users to also include autogenerated code. This is also a downside that I see.
So right now I am thinking how important we value performance over usage/simplicity. I fiddled around with Kotlin a little bit to find out a way to easily create entities/components and to access them. The way systems/families are working right now, is pretty intuitive and nice imo and I want to keep that.
Here is a quick&dirt implementation and a draft of how Fleks 2.0 could look like (including a very simple benchmark):
The benchmark is basically the "ADD_REMOVE" benchmark of Fleks. Of course performance-wise it is worse but I think it is not that bad. On my machine it has a minimum of 2 milliseconds, which is the same as Fleks has at the moment. The average is ~12ms which is about half the speed of Fleks right now.
HOWEVER, I do like the syntax a lot and I think for users it is also easier to understand and debug the Entity class which already contains all the components of that specific entity. The "magic" is following part:
The
ComponentType
is like the unique ID of each type of component (like the ID I described above). A component then needs to implement the newComponent
interface. You can simply copy&paste the two necesarry lines to each component or create a live template for it in IntelliJ. Again, this is a little bit annoying but I think it is acceptable. With that way we can achive an API like this:Another benefit of this approach is that it requires no Reflection and is therefore usable for KMP as well. Also, the component creation is not "magically" happening in the background of the framework. That way the user can already create and modify the component directly the way he needs it. In my opinion we might be able to remove
ComponentListener
that way. At least the adding makes no more sense to me. The removal might be nice still, that's why I added it to the quick&dirt draft above.Also, with this approach it is no longer necessary for KMP to define a factory method for every component.
Finally, it would even be possible to attach the same component multiple times to an entity which is currently not possible in Fleks. Imagine like you have an entity with multiple sprites. At the moment you can solve that by giving your sprite component a list of sprites. With this approach you can simply define different
ComponentType
"keys" in the Sprite component like "BaseSprite", "WeaponSprite", "HeadSprite", ... and then just add them to the entity. The entity then has multiple Sprite components that can be accessed via the different types.@jobe-m / @mariorez: What do you think about that? Shall we move in such a direction in the future for Fleks, knowing that the performance will most likely be a little bit worse (but of course still faster than Ashley at least ;) ).
Do you have any suggestions or pain points right now?
edit: some tryout on the playground from my phone. Just another draft that might help:
Beta Was this translation helpful? Give feedback.
All reactions