A fast, lightweight, entity component system library written in Kotlin.

Overview

Fleks

A fast, lightweight, entity component system library written in Kotlin.

Motivation

When developing my hobby games using LibGDX, I always used Ashley as an Entity Component System since it comes out of the box with LibGDX and performance wise it was always good enough for me.

When using Kotlin and LibKTX you even get nice extension functions for it but I never was fully happy with how it felt because:

Defining ComponentMapper for every Component felt very redundant

  • Ashley is not null-safe and therefore you get e.g. Entity? passed in as default to an IteratingSystem although it will never be null (or at least shouldn't 😉 )
  • Creating Families as constructor arguments felt weird to me
  • The creator seems to not work on this project anymore or at least reacts super slowly to change requests, bugs or improvement ideas
  • Although for me it was never a problem, I heard from other people that the performance is sometimes bad especially with a lot of entities that get their components updated each frame

Those are the reasons why I wanted to build my own ECS-library and of course out of interest to dig deep into the details of this topic and to learn something new!

Who should use this library?

If you need a lightweight and fast ECS in your Kotlin application then feel free to use Fleks.

If you are looking for a fully fledged ECS that supports almost anything that you can imagine and you don't care about Kotlin then use Artemis-odb or Ashley.

Current Status

There is no official release yet and the library is still under construction. Please feel free to contribute to the Discussions or Issues. Help is always appreciated.

Current API and usage (not final)

World

The core of Fleks is the World which is the container for entities, components and systems and is the object that you need to update your systems.

To create a world simply call:

val world = World {}

A world without any system doesn't make sense and that's why there is a lambda argument for the world's constructor to configure it accordingly:

  • Use entityCapacity to set the expected maximum amount of entities. The default value is 512. The reason for this setting is to initialize internal collections and arrays with a proper size for your game to avoid a lot of Array.copy calls in the background which are slow.
  • Use system to add a system to your world. The order of system calls defines the order in which they are called when calling world.update

Here is an example that creates a world for 1000 entities with a Move- and PhysicSystem:

val world = World {
    entityCapacity = 1000

    system<MoveSystem>()
    system<PhysicSystem>()
}

System

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 IteratingSystem 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: Float) {
        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 inject function:

val eventManager = EventManager()
val world = World {
    entityCapacity = 1000

    system<DayNightSystem>()

    inject(eventManager)
}

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:

  • tickRate: defines the time in milliseconds when the system should be updated. Default is 0 means that it gets called every time world.update is called
  • 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 class annotations 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

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 onEntityAction(entityId: Int, deltaTime: Float) {
        // 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.

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 onEntityAction(entityId: Int, deltaTime: Float) {
        val entityPosition: Position = positions[entityId]
    }
}

Entity and Components

We now know how to create a world and add systems to it, but we don't know how to add entities to our world. This can be done via the entity function of the world. Let's create an entity with a PositionComponent and SpriteComponent:

{ x = 5f } add () } } ">
data class Position(var x: Float = 0f, var y: Float = 0f)

data class Sprite(var texturePath: String = "")

fun main() {
    val world = World {}

    val entityId: Int = world.entity {
        add<Position> { x = 5f }
        add<Sprite>()
    }
}

Performance

One important topic for me throughout the development of Fleks was performance. For that I compared Fleks with Artemis-odb and Ashley in three scenarios:

  1. AddRemove: creating 10_000 entities with a single component each and removing those entities
  2. Simple: stepping the world 1_000 times for 10_000 entities with an IteratingSystem for a single component that gets a Float counter increased by one every time
  3. Complex: stepping the world 1_000 times for 10_000 entities, two IteratingSystems and three components. It is a time-consuming benchmark because all entities get added/removed from the first system each update call.
    • Each entity gets initialized with ComponentA and ComponentC.
    • The first system requires ComponentA, ComponentC and not ComponentB. It switches between creating ComponentB or removing ComponentA. That way every entity gets removed from this system each update call.
    • The second system requires any ComponentA/B/C and removes ComponentC and adds ComponentA. That way every entity gets added again for the first system.

I used kotlinx-benchmark to create the benchmarks. Measurement is number of executed operations within 3 seconds.

All Benchmarks are run within IntelliJ using the benchmarksBenchmark gradle task on my local computer. The hardware is:

  • Windows 10 64-bit
  • 16 GB Ram
  • Intel i7-5820K @ 3.30Ghz
  • Java 8 target
  • Kotlin 1.5.31

Here is the result (the higher the Score the better):

Library Benchmark Mode Cnt Score Error Units
Ashley AddRemove thrpt 3 207,007 ± 39,121 ops/s
Ashley Simple thrpt 3 3,986 ± 1,390 ops/s
Ashley Complex thrpt 3 0,056 ± 0,117 ops/s
Artemis AddRemove thrpt 3 620,550 ± 1244,013 ops/s
Artemis Simple thrpt 3 32,830 ± 2,965 ops/s
Artemis Complex thrpt 3 1,452 ± 0,452 ops/s
Fleks AddRemove thrpt 3 1904,151 ± 530,426 ops/s
Fleks Simple thrpt 3 33,639 ± 5,651 ops/s
Fleks Complex thrpt 3 1,063 ± 0,374 ops/s

I am not an expert for performance measurement, that's why you should take those numbers with a grain of salt but as you can see in the table:

  • Ashley is the slowest of the three libraries by far
  • Fleks is ~300% the speed of Artemis in the AddRemove benchmark
  • Fleks is ~the same speed as Artemis in the Simple benchmark
  • Fleks is ~70% the speed of Artemis in the Complex benchmark

As an additional note please be aware that Fleks does not support all the functionalities that the other two libraries offer. Fleks' core is very small and simple and therefore it does not need to process as much things as Ashley or Artemis might. Still, in those benchmarks all libraries have to fulfill the same need which reflects some common tasks in my own games.

Comments
  • Fleks 2.0

    Fleks 2.0

    Progress:

    • [x] Breakthrough
    • [x] Code adjustments
    • [x] Unit test adjustments
    • [x] Update comments
    • [x] Update wiki
    • [x] Update ReadMe
    • [x] Update version and combine JVM and KMP. Maybe make the current master a new JVM_history branch to keep the history
    • [x] Verify new version in Dinoleon and Mystic Woods

    This is currently a WIP for Fleks 2.0. I did most of the code adjustments but I am still missing the unit test adjustments and comments.

    I did a test driven development approach (fleks2-tdd.kt) which I kinda liked for this exercise. It helped to get the API faster in the way I wanted it.

    Also, I want to discuss the changes that I have applied, if they make sense to everyone or not. Be aware, here comes a wall of text ;) Questions are highlighted in bold and with a ❓ emoji.

    To easily create the boilerplate code for the new Fleks component approach, I suggest to create a Live Template in the IDE. For example in IntelliJ we can do the following. You then just type flekscomponent or whatever abbreviation you want to use to get the necessary code. image

    Template text

    class $FLEKS_COMPONENT$ : Component<$FLEKS_COMPONENT$> {
    	override fun type() = $FLEKS_COMPONENT$
    
    	companion object : ComponentType<$FLEKS_COMPONENT$>()
    }
    

    TLDR: imo the Fleks codebase gets a little smaller and easier to understand with those changes. Also, with the removal of reflection and factory methods it is easier to understand, use and debug. The performance is slightly faster than before. I guess because of the removal of factory method calls and the conversion from listeners to hooks.


    Changes

    Components

    It is no longer necessary to add components to the WorldConfiguration. ComponentMapper are created automatically similar to the JVM version, and we also no longer need any reflection or factory methods.

    In order for that to work, a new interface and abstract class was added and every component now must implement the new Component interface. More about the benefits below in the "Systems" section.

    A position component now looks like this:

    data class Position(
        var x: Float,
        var y: Float,
    ) : Component<Position> {
        override fun type(): ComponentType<Position> = Position
    
        companion object : ComponentType<Position>()
    }
    

    The ComponentType is important to give each type of component a unique ID which is necessary for Fleks internal stuff to optimize the way components are stored.

    With Kotlin's unnamed companion object we can then achieve imo some nice syntax sugar (see below in "Systems" section).

    With this change it is now also possible to add the same component multiple times to an entity. This is not possible in Fleks 1.x. This is something new! :) Here is an example of a Sprite component that can either be a background or foreground of an entity:

    ata class Sprite(
        val background: Boolean,
        var path: String = "",
    ) : Component<Sprite> {
        override fun type(): ComponentType<Sprite> {
            return when (background) {
                true -> SpriteBackground
                else -> SpriteForeground
            }
        }
    
        companion object {
            val SpriteForeground = object : ComponentType<Sprite>() {}
            val SpriteBackground = object : ComponentType<Sprite>() {}
        }
    }
    

    Because type() is a function, we can basically do whatever we want and therefore reuse the same component class for different purposes.

    Systems

    Since we cannot get annotations in KMP, I adapted the existing approach of defining the family configuration in a system's constructor. For that, I added a new family function with a new FamilyDefinition configuration to define the family and removed the allOf, anyOf, noneOf arguments with a single family argument.

    This has two advantages imo:

    1. Users must provide a family and cannot forget it anymore. Before this was not immediately visible because allOf, anyOf and noneOf had a default to an empty collection.
    2. The amount of constructor arguments for an IteratingSystem gets reduced

    Here is an example of the new syntax:

    private class PositionSystem : IteratingSystem(family { all(Position) }) {
        override fun onTickEntity(entity: Entity) {
            entity[Position].x++ // <-- new way of retrieving components
        }
    }
    

    Note, that ComponentMapper are no longer necessary from a user's point of view. Via entity[Position] we can now simply access the position component of an entity.

    Also, family definitions no longer need the ::class syntax. The allI, none and any functions of a FamilyDefinition take a vararg of ComponentType.

    Shall we use the familyDefinition approach? Or is the previous approach nicer?


    The configureEntity function got replaced with an extension function for an Entity. Instead of writing configureEntity(entity) { ... } it is now entity.configure { ... }. This is a minor change but I think it is easier to read. Also, the configuration API got changed from mappers to the new component approach. Here is an example of one of the benchmark systems that removes a FleksPosition component and adds a FleksLife component:

    class FleksSystemComplex1 : IteratingSystem(family { all(FleksPosition).none(FleksLife).any(FleksSprite) }) {
        private var actionCalls = 0
    
        override fun onTickEntity(entity: Entity) {
            if (actionCalls % 2 == 0) {
                entity[FleksPosition].x++
                entity.configure { it += FleksLife() } // <-- new way of adding components
            } else {
                entity.configure { it -= FleksPosition } // <-- new way of removing  components
            }
            entity[FleksSprite].animationTime++
            ++actionCalls
        }
    }
    

    Finally, a new compareEntityBy function was added for easier sorting. You can now make any component implement the Comparable interface and then write the sorting code in that specific component. With that you can achieve something like this:

    private data class SystemTestComponent(
        var x: Float = 0f
    ) : Component<SystemTestComponent>, Comparable<SystemTestComponent> {
        override fun type() = SystemTestComponent
    
        override fun compareTo(other: SystemTestComponent): Int {
            return other.x.compareTo(x)
        }
    
        companion object : ComponentType<SystemTestComponent>()
    }
    
    private class SystemTestIteratingSystemSortManual : IteratingSystem(
        family = family { all(SystemTestComponent) },
        comparator = compareEntityBy(SystemTestComponent), // <-- easy and concise way to create an EntityComparator
        sortingType = Manual
    ) {
        // ... rest of the code omitted
    }
    

    Entities

    As seen above, entity configuration and also the creation got adjusted to the new components implementation. Here are some examples:

        @Test
        fun testComponentHooks() {
            val addComponent = Position(0f, 0f)
            val removeComponent = Position(0f, 0f)
            lateinit var testWorld: World
            testWorld = world {
                components {
                    // example of an onAdd hook for a position component. 'component' is of type Position
                    onAdd(Position) { world, entity, component ->
                        component.x = 1f
                        assertEquals(testWorld, world)
                        assertTrue { entity.id in 0..1 }
                        assertTrue { component in listOf(addComponent, removeComponent) }
                    }
    
                    // example of an onRemove hook for a position component. 'component' is of type Position
                    onRemove(Position) { world, entity, component ->
                        component.x = 2f
                        assertEquals(testWorld, world)
                        assertEquals(Entity(1), entity)
                        assertEquals(removeComponent, component)
                    }
                }
            }
    
            // entity that triggers onAdd hook
            testWorld.entity { it += addComponent }
            // entity that triggers onRemove hook
            val removeEntity = testWorld.entity { it += removeComponent }
            testWorld.configure(removeEntity) { it -= Position }
    
            assertEquals(1f, addComponent.x)
            assertEquals(2f, removeComponent.x)
        }
    
        @Test
        fun testFamilyHooks() {
            lateinit var testWorld: World
            lateinit var testFamily: Family
            var numAddCalls = 0
            var numRemoveCalls = 0
            testWorld = world {
                testFamily = family { all(Position) }
                families {
                    // example of an onAdd hook for a family with allOf=Position
                    onAdd(testFamily) { world, entity ->
                        ++numAddCalls
                        assertEquals(testWorld, world)
                        assertTrue { entity.id in 0..1 }
                    }
    
                    // example of an onRemove hook for a family with allOf=Position
                    onRemove(testFamily) { world, entity ->
                        ++numRemoveCalls
                        assertEquals(testWorld, world)
                        assertEquals(Entity(1), entity)
                    }
                }
            }
    
            // entity that triggers onAdd hook
            testWorld.entity { it += Position(0f, 0f) }
            // entity that triggers onRemove hook
            val removeEntity = testWorld.entity { it += Position(0f, 0f) }
            testWorld.configure(removeEntity) { it -= Position }
            // trigger family update to call the hooks
            testFamily.updateActiveEntities()
    
            assertEquals(2, numAddCalls)
            assertEquals(1, numRemoveCalls)
        }
    

    ComponentListeners and FamilyListeners

    I thought about this and in my opinion it is actually useless to have a list of listeners because why would someone register multiple listeners for the same component/family? At least I never used that and I could not come up with why I would ever do that.

    That's why I changed the listener approach to a "hook" approach. It is basically the same but you specify only one add/remove hook per component/family. That way we don`t need to iterate over a list of listeners all the time which always has the size 1.

    Important to note is that a hook always gets the world instance as an argument and the world now keeps the registered injectables and does not remove them anymore after creation. That way you can still get any relevant object into your hook code.

    Here is an example of a component and a family hook. Fleks distinguishes between an add and remove hook. It is not necessary to add the code directly to the WorldConfiguration. It can of course link to a function in a different file.

        @Test
        fun testComponentHooks() {
            val addComponent = Position(0f, 0f)
            val removeComponent = Position(0f, 0f)
            lateinit var testWorld: World
            testWorld = world {
                components {
                    // example of an onAdd hook for a position component. 'component' is of type Position
                    onAdd(Position) { world, entity, component ->
                        component.x = 1f
                        assertEquals(testWorld, world)
                        assertTrue { entity.id in 0..1 }
                        assertTrue { component in listOf(addComponent, removeComponent) }
                    }
    
                    // example of an onRemove hook for a position component. 'component' is of type Position
                    onRemove(Position) { world, entity, component ->
                        component.x = 2f
                        assertEquals(testWorld, world)
                        assertEquals(Entity(1), entity)
                        assertEquals(removeComponent, component)
                    }
                }
            }
    
            // entity that triggers onAdd hook
            testWorld.entity { it += addComponent }
            // entity that triggers onRemove hook
            val removeEntity = testWorld.entity { it += removeComponent }
            testWorld.configure(removeEntity) { it -= Position }
    
            assertEquals(1f, addComponent.x)
            assertEquals(2f, removeComponent.x)
        }
    
        @Test
        fun testFamilyHooks() {
            val testFamilyDef = familyDefinition { allOf(Position) }
            var numAddCalls = 0
            var numRemoveCalls = 0
            lateinit var testWorld: World
            testWorld = world {
                families {
                    // example of an onAdd hook for a family with allOf=Position
                    onAdd(testFamilyDef) { world, entity ->
                        ++numAddCalls
                        assertEquals(testWorld, world)
                        assertTrue { entity.id in 0..1 }
                    }
    
                    // example of an onRemove hook for a family with allOf=Position
                    onRemove(testFamilyDef) { world, entity ->
                        ++numRemoveCalls
                        assertEquals(testWorld, world)
                        assertEquals(Entity(1), entity)
                    }
                }
            }
    
            // entity that triggers onAdd hook
            testWorld.entity { it += Position(0f, 0f) }
            // entity that triggers onRemove hook
            val removeEntity = testWorld.entity { it += Position(0f, 0f) }
            testWorld.configure(removeEntity) { it -= Position }
            // trigger family update to call the hooks
            testWorld.family(testFamilyDef).updateActiveEntities()
    
            assertEquals(2, numAddCalls)
            assertEquals(1, numRemoveCalls)
        }
    

    Shall we keep the hook approach? Or shall we go back to the listeners approach?

    Injectables

    As mentioned before, injectables are no longer cleared after the world is created. The main reason is the new hook approach.

    But there are also other changes like e.g. the removal of the Inject object. Since the world is now keeping references to the different injectables, we can directly use the world to inject stuff. Also, it is now possible to do constructor DI instead of just property DI. Imo constructor DI is always preferable because it creates cleaner code and makes it easier to test classes when you directly see their dependencies. In order for that to work, the world creation process was adjusted a little bit and a new inject function was added.

    Here is an example of a system that showcases both DI mechanisms:

    private class SpriteSystem(
        val cstrInjectable: String = inject() // <-- constructor DI via the global inject function
    ) : IteratingSystem(family { any(SpriteBackground, SpriteForeground) }) {
        val propInjectable: String = world.inject("qualifiedString") // property DI via world.inject. The global function will work here as well
    
        override fun onTickEntity(entity: Entity) = Unit
    }
    

    WorldConfiguration

    As already seen in some examples above, the configuration of the world has also changed slightly because of the removal of the listeners but also, because of the removal of factory methods for systems. Systems are now directly created which makes it imo easier to understand the code and also to debug the creation process, if necessary.

    It is important, that the systems block comes last in the configuration when family or component hooks are registered. Fleks takes care of that and throws a new FleksWrongConfigurationOrder exception if users fail to do so. The reason is that system constructors can create new entities and if user hooks are registered after system creation, then of course they are not called correctly for such entities.

    Here is an example test case with component hooks and a system:

        @Test
        fun notifyComponentHooksDuringSystemCreation() {
            var numAddCalls = 0
            var numRemoveCalls = 0
    
            world {
                components {
                    onAdd(WorldTestComponent) { _, _, _ -> ++numAddCalls }
                    onRemove(WorldTestComponent) { _, _, _ -> ++numRemoveCalls }
                }
    
                systems {
                    add(WorldTestInitSystem())
                }
            }
    
            assertEquals(1, numAddCalls)
            assertEquals(0, numRemoveCalls)
        }
    
    opened by Quillraven 51
  • Update KMP with changes of 1.1-JVM to 1.3-JVM

    Update KMP with changes of 1.1-JVM to 1.3-JVM

    Changelog:

    • [x] BUGFIX: Entities created inside a system's constructor are now properly added to their family and also EntityListener get notified correctly in this case
    • [x] BUGFIX: ComponentListener did not get notified when entities are created in a system's init block
    • [x] UPDATE: change dokka from 1.6.10 to 1.6.21
    • [x] UPDATE: the used parameter of an injectable is removed because injectables are now properly resolved and cleared once the world is created
    • [x] NEW: make world a default dependency for the dependency injection mechanism. That way it is now possible to easily pass the world to a ComponentListener
    • [x] NEW: add getter for world's systems
    • [x] NEW: add configureEntity to world. This makes it now possible to modify an entity outside of a system as requested in the comments section of my YouTube channel
    • [x] NEW: add support to create families out of a system via the world. This is the biggest change of this version and adds new flexibility on how to handle and iterate over entities with specific component configurations
    • [x] NEW: users have now access to the family of an IteratingSystem
    • [x] NEW: family now also contains a configureEntity function which allows users to modify an entity outside of a system in a convenient way
    • [x] NEW: a new FamilyListener support is added which allows users to react on when an entity is added to, or removed from a family
    • [x] NEW: new DSL to create a world. Use components , systems, injectables or families block to define your world. For more details, refer to the ReadMe
    • [x] NEW: the KMP setup was updated to a proper multiplatform gradle setup. That way it is now possible to add Fleks to a KorGE dependency via following line in the korge block
      dependencyMulti("io.github.quillraven.fleks:Fleks:1.3-KMP", registerPlugin = false)
      

    This PR contains all the changes that were missing in the KMP branch that got added in the 1.1-JVM, 1.2-JVM and 1.3-JVM version.

    In addition it changes the gradle setup to be a real KMP project. Most of the code is in commonMain but there are some testcases in other source sets because when it comes to exceptions then the different backends are behaving differently.

    Not sure if this is the best solution to do it but for now let's roll with it ;) As an example, in JS there is no IndexOutOfBoundsException when accessing an invalid array position. It won't show any error but the result is most likely undefined.

    It also contains some changes that were done in this PR: https://github.com/Quillraven/Fleks/pull/37


    With the new setup it should be now possible to add Fleks as a normal dependency to KorGE like this:

    dependencyMulti("io.github.quillraven.fleks:Fleks:1.3-KMP", registerPlugin = false)
    
    help wanted 
    opened by Quillraven 21
  • Make Fleks multi platform

    Make Fleks multi platform

    I am not an expert when it comes to Kotlin MPP but I guess it should not be super difficult to convert the current code to MPP? Would be great if we can achieve this to support any backend.

    Please share good articles/tutorials about MPP or create a PR yourself! Would be much appreciated.

    help wanted 
    opened by Quillraven 19
  • Missing BitArray toString function

    Missing BitArray toString function

    In general we should improve the way to debug and analyze Fleks. Imo it is currently impossible to understand which bit of a BitArray is set which makes it hard to e.g. debug entities of a family before the bag gets updated or the component mask of an entity.

    In addition, it is not comfortable to immediately see which entities have which components because of the performant and efficient way things are stored internally.

    I am thinking of a toMap function for the world where the key is the entity and the value is a list of components of that entity.

    Such a map can e.g. easily be converted to a JSON string for serializing but it also allows an easy overview of the current state of the world, it's entities and its components.

    That should not be hard to implement with the information that the EntityService already has. It knows the active entities and the component mask of each entity which is the ComponentMapper ID of each component of the entity. The mapper can already be retrieved via the ID with the ComponentService.

    enhancement 
    opened by Quillraven 13
  • Merge master branch content into next branch

    Merge master branch content into next branch

    The content on the next branch should not be the same as for release 1.2-JVM. After some testing we could publish release 1.2-KMP.

    I will also try to use directly this version in my stand alone Korge game. If that works I could drop Korge-fleks again.

    opened by jobe-m 11
  • Feature Request: API to get a family of entities.

    Feature Request: API to get a family of entities.

    I want a way to be able to easily get a family of entities with certain components that's decoupled from the systems.

    Example:

            val familyContainer = world.familyContainer(
                allOf = ...,
                anyOf = ...,
                noneOf = ...,
            )
            val entities: List<Entity> = family.getEntities()
    

    The family container should be automatically updated on addition/removal of entities and addition/removal of their components.

    enhancement 
    opened by Kietyo 11
  • Combine Fleks master/kmp version into a single version

    Combine Fleks master/kmp version into a single version

    Fleks has two separate versions at the moment:

    • JVM backend - master branch
    • Kotlin Multiplatform - kmp branch

    The future, in my opinion, is the kmp branch which currently has a slightly different API than the master branch.

    The things that we should have a look at is:

    • How to avoid that users need to register components to the world as this is a little bit redundant and tidious setup
    • In general, is it possible to get rid off the WorldConfiguration? If we can remove the component setup from above then it might be possible to also remove ComponentListener and System setup. The only thing left would be the entity capacity which can be directly passed to the world and the dependency injection (see next point).
      • As an idea: introduce new @Component and @System(priority : int) annotations. Users can add them to their component/system classes and via annotation processing we can maybe automatically register them to the world.
    • If we go for the annotation processing route then it is maybe also possible to replace Inject.dependency and Inject.componentMapper with an annotation to be consistent.
      • Idea: here we can maybe go the other route and mark injectables with an @Injectable annotation. They will then be collected during the processing step and can be directly passed to the system constructors?
    • Optional: bring back the family annotations instead of using properties in the system

    As an alternative, if we do not want to go for the annotations route, we could think about a declarative api but then we should maybe remove the dependency injection part to create something like:

    val world = World {
      lateinit var boundaryMapper : ComponentMapper<BoundaryComponent>
      lateinit var moveMapper: ComponentMapper<MoveComponent>
      lateinit var renderMapper: ComponentMapper<RenderComponent>
    
      components {
        boundaryMapper  = add<BoundaryComponent>()
        moveMapper = add<MoveComponent>()
        renderMapper = add<RenderComponent>()
      }
    
      systems {
        MoveSystem(textureAtlas, eventManager, moveMapper, boundaryMapper ) {
          allOf<MoveComponent, BoundaryComponent>
        }
        RenderSystem(spriteBatch, camera, boundaryMapper, renderMapper) {
          allOf<BoundaryComponent, RenderComponent>
        }
      }
    }
    

    The advantage of this version is that no magic is happening and it describes everything you need to know in an understandable way. The disadvantage is that a few things are redundant like e.g. I personally do not like that I have to add components or create ComponentMapper. It would be great if defining a Component class would be sufficient somehow.

    help wanted 
    opened by Quillraven 11
  • Collection functions for entities

    Collection functions for entities

    As per the #66 discussion, this PR is updating the existing IntBag functionality with certain Kotlin collection stdlib functions.

    Following things are adjusted:

    • rename IntBag to EntityBag: this change also includes converting the underlying IntArray to Array<Entity> and changing Entity back to a value class. For some reason it is a data class (I guess this is some leftover of the previous KMP branch changes)
    • rename entityBag inside family to entities and expose it (internal -> public)
    • add a new asEntityBag function to the world
    • add collection stdlib functions to EntityBag

    Additionally, this PR updates Kotlin and Dokka from 1.7.10 to 1.7.20.

    Code adjustments are done but I am missing the tests currently and the documentation of the new functions. Following functions are now available for EntityBag:

    • size: this is actually a property
    • contains
    • containsAll
    • isEmpty, isNotEmpty
    • all, any, none
    • associate, associateBy, associateTo, associateByTo
    • count
    • filter, filterNot, filterIndexed, filterTo, filterNotTo, filterIndexedTo
    • find
    • first, firstOrNull
    • fold, foldIndexed
    • forEach, forEachIndexed
    • map, mapIndexed, mapTo, mapIndexedTo
    • random, randomOrNull
    • take
    opened by Quillraven 9
  • Update with changes done in korge-fleks

    Update with changes done in korge-fleks

    New feature was added which makes it possible to mark an injectable as "used". Thus Fleks will not throw exception if the injectable is not used directly at creation of system and listener objects. Ie. that makes it possible to use the injected objects during init functions. Default for use is still "false" as before.

    opened by jobe-m 8
  • Make Fleks multiplatform compatible

    Make Fleks multiplatform compatible

    • Adapt registering of systems in World

    Instantiation of generic classes is not possible in Kotlin multiplatform. Thus, instead of instantiating a generic class in the SystemService the system function gets a factory method for calling the constructor of the system.

    • Introduce adding of component and its listener in world configuration

    The component listener will be added together with registering the component in the world configuration. That was done because when registering the component listener the base type of the component is needed. But without java reflections it is not possible to detect the type of component listeners. Thus, adding component and listener objects together is the most elegant workaround for now - I hope.

    • Adapted readme to reflect changed API

    • Adapted Fleks benchmark

    ~~Currently registering of systems is not working in the benchmark fleks.kt. In my Korge example it is working that way. It could be that this is due to some multiplatform configuration in Korge. But currently this is beyond my understanding. :(~~

    Stupid mistake. Now the benchmark is working. Only the complex one is failing due to an exeption.

    • Renaming some variables

    I changed cmp to comp because especially at the beginning I read e.g. cmpMapper as "compareMapper" and not "componentMapper" which was a bit confusing.

    opened by jobe-m 8
  • 2.1 family doesn't get updated

    2.1 family doesn't get updated

    Hi Simon, I think I found a bug in the 2.1 version. So I have a extension that check if a certain family is in a position:

    fun Position.findEntitiesInPosition(family: Family): List<Entity> {
        val entities = mutableListOf<Entity>()
        family.forEach {
            if (it.has(Position)
                && it[Position].x == x
                && it[Position].y == y
            ) {
                entities.add(it)
            }
        }
        return entities
    }
    

    I use this to see if I can move to a target position

    class MoveSystem(
        private val maxWidth:Int = inject("maxWidth"),
        private val maxHeight:Int = inject("maxHeight")
    ) : IteratingSystem(family { all(Position, Moving).none(Dead)}) {
        override fun onTickEntity(entity: Entity) {
            val originalPosition = entity[Position]
            val targetPosition = entity[Moving].target
            // position is within the limit and position is not occupied
            if (targetPosition.positionWithin(maxWidth, maxHeight)
                && targetPosition.findEntitiesInPosition(world.family { all(Position, Solid) }).isEmpty()){
                originalPosition.x = targetPosition.x
                originalPosition.y = targetPosition.y
            }
    
            entity.configure { it -= Moving }
        }
    
    

    So for the version 2.0 this was working fine but I recently upgraded to 2.1 and now it is failing the test below:

        @Test
        fun shouldMoveIntoADeadObject(){
            val x = 5
            val y = 5
            val original = Position(x, y)
            val target = Position(8,8)
            val entity = world.entity {
                it += original
                it += Moving(target)
            }
    
            val toDie = world.entity{
                it += target
                it += Solid()
            }
    
            world.update(1f)
            // Check I cannot move
            Assertions.assertEquals(original, Position(x,y))
            Assertions.assertNotEquals(original, target)
    
            with(world){
                toDie.configure {
                    it -= Solid
                    it += Dead()
                }
                entity.configure { it += Moving(target) }
            }
    
            world.update(1f)
            // Check it should have moved
            Assertions.assertEquals(original, target)
        }
    
        
    
    bug 
    opened by ernespn 7
Releases(2.2)
  • 2.2(Nov 26, 2022)

    • BUGFIX: fixed an issue that was introduced in version 2.1 which caused families to not get updated properly when used in a nested iteration way. Thanks to @ernespn for reporting this issue!
    • NEW: added the get operator to the EntityBag interface. This allows to do a normal for loop over a family. Thanks to @ScottHamper for the proposal and contribution!
    • UPDATE: updated Kotlin to 1.7.21
    Source code(tar.gz)
    Source code(zip)
  • 2.1(Oct 23, 2022)

    A small update that brings some collection functionality when working with entities (EntityBag):

    • UPDATE: Kotlin and Dokka to 1.7.20
    • UPDATE: change Entity back to value class instead of data class
    • UPDATE: nested calls of entity and entity.configure are now possible. Also, the documentation of those two functions got updated to point out a potential risk.
    • NEW: add entity contains function to family
    • UPDATE: IntBag is now split into two classes: EntityBag and MutableEntityBag. You can use them in your own game if you want.
    • NEW: add a lot of collection functionality to EntityBag (thanks to @rubybrowncoat for the suggestion):
      • size
      • contains, containsAll
      • isEmpty, isNotEmpty
      • all, any, none
      • associate, associateBy, associateTo, associateByTo
      • count
      • filter, filterNot, filterIndexed, filterTo, filterNotTo, filterIndexedTo
      • find
      • first, firstOrNull
      • fold, foldIndexed
      • forEach, forEachIndexed
      • map, mapIndexed, mapTo, mapIndexedTo, mapNotNull, mapNotNullTo
      • random, randomOrNull
      • take
      • groupBy, groupByTo
      • flatMap, flatMapSequence, flatMapBag, flatMapNotNull, flatMapSequenceNotNull, flatMapBagNotNull
    Source code(tar.gz)
    Source code(zip)
  • 2.0(Sep 28, 2022)

    After almost one year, this release finally combines the JVM and KMP releases into one single release. I also tried to simplify Fleks for newcomers and be more consistent with the way of Kotlin, resulting in more concise and readable code.

    This has the sideeffect, that there are several breaking changes in version 2.0 and you cannot simply upgrade from either 1.6-JVM or 1.6-KMP because of those API changes.

    For more details, please check out the wiki which contains both, the hold and new API documentation.

    If you want to migrate then also please have a look at this youtube video from myself, where I migrate some code segments from 1.6-JVM to 2.0.

    I hope you like the new changes and try it out!

    Special shout outs to @jobe-m and @czyzby who helped with the development of 2.0 by giving feedback and suggestions. And also for the verification in a KorGE game setup (jobe-m). Thanks!

    Source code(tar.gz)
    Source code(zip)
  • 1.6-KMP(Aug 19, 2022)

    This release fixes a rare bug and improves the debugging capabilities of Fleks. In addition, it is now possible to load a specific "snapshot" for your world.

    • BUGFIX: fix an issue that entities did not get removed from families that only used the noneOf configuration. The reason is that a removed entity has no components which is then actually valid for such families. However, a removed entity should of course be removed from any family ;)
    • UPDATE: BitArray has a new toString function which makes it easier to debug certain things.
    • NEW: new snapshot and snapshotOf function for the World to get a snapshot of the entire world (=entities and components) or of a specific entity. This should also help with debugging. A new page to the Wiki was added as well regarding debugging.
    • NEW: new loadSnapshot function for the World to load a specific snapshot. This will clear the current world and create entities and components according to the provided snapshot.
    • Gradle got updated from 7.5 to 7.5.1
    Source code(tar.gz)
    Source code(zip)
  • 1.6-JVM(Aug 16, 2022)

    This release fixes a rare bug and improves the debugging capabilities of Fleks. In addition, it is now possible to load a specific "snapshot" for your world.

    • BUGFIX: fix an issue that entities did not get removed from families that only used the noneOf configuration. The reason is that a removed entity has no components which is then actually valid for such families. However, a removed entity should of course be removed from any family ;)
    • UPDATE: BitArray has a new toString function which makes it easier to debug certain things.
    • NEW: new snapshot and snapshotOf function for the World to get a snapshot of the entire world (=entities and components) or of a specific entity. This should also help with debugging. A new page to the Wiki was added as well regarding debugging.
    • NEW: new loadSnapshot function for the World to load a specific snapshot. This will clear the current world and create entities and components according to the provided snapshot.
    • Gradle got updated from 7.5 to 7.5.1

    As always, KMP version will be released a few days later.

    Source code(tar.gz)
    Source code(zip)
  • 1.5-KMP(Jul 25, 2022)

    This is a small release that brings in an important bugfix, some utility functions for families and some internal version upgrades:

    • BUGFIX: fixed numEntities of a family to return the correct number of entities instead of the highest ID of any entity inside the family
    • NEW: added isEmpty, isNotEmpty, first and firstOrNull functions to a family
    • Update Kotlin version to 1.7.10
    • Update Gradle to 7.5
    Source code(tar.gz)
    Source code(zip)
  • 1.5-JVM(Jul 24, 2022)

    This is a small release that brings in an important bugfix, some utility functions for families and some internal version upgrades:

    • BUGFIX: fixed numEntities of a family to return the correct number of entities instead of the highest ID of any entity inside the family
    • NEW: added isEmpty, isNotEmpty, first and firstOrNull functions to a family
    • Update Kotlin version to 1.7.10
    • Update Gradle to 7.5
    Source code(tar.gz)
    Source code(zip)
  • 1.4-JVM(Jul 5, 2022)

    This is a small update to the JVM version containing the new DSL to create and configure a world, which was added in the latest KMP version. JVM and KMP are now up-to-date again featurewise.

    Be aware, that this version contains a breaking change when it comes to creating a world. This must be done via the new DSL.

    Also, the wiki got updated and the API/example area is now removed from the ReadMe file. Everything got moved to the wiki which also contains the documentation for the new DSL!

    Source code(tar.gz)
    Source code(zip)
  • 1.4-KMP-RC1(Jun 28, 2022)

    One bigger release for the KMP version of Fleks bringing all the changes done in the JVM version since 1.1 into it. In addition, a new DSL was added to configure a world in a nicer way. Also, the project setup was converted from a JVM project to a real Kotlin multiplatform project with different sourceSets for the different targets. This makes it now possible to add Fleks via normal dependencies to a KMP project like e.g. in a KorGE game.

    Refer to the full changelog below:


    Changelog:

    • BUGFIX: Entities created inside a system's constructor are now properly added to their family and also EntityListener get notified correctly in this case
    • BUGFIX: ComponentListener did not get notified when entities are created in a system's init block
    • UPDATE: change dokka from 1.6.10 to 1.6.21
    • UPDATE: the used parameter of an injectable is removed because injectables are now properly resolved and cleared once the world is created
    • UPDATE: minor memory management improvements
    • NEW: make world a default dependency for the dependency injection mechanism. That way it is now possible to easily pass the world to a ComponentListener
    • NEW: add getter for world's systems
    • NEW: add configureEntity to world. This makes it now possible to modify an entity outside of a system as requested in the comments section of my YouTube channel
    • NEW: add support to create families out of a system via the world. This is the biggest change of this version and adds new flexibility on how to handle and iterate over entities with specific component configurations
    • NEW: users have now access to the family of an IteratingSystem
    • NEW: family now also contains a configureEntity function which allows users to modify an entity outside of a system in a convenient way
    • NEW: a new FamilyListener support is added which allows users to react on when an entity is added to, or removed from a family
    • NEW: new DSL to create a world. Use components , systems, injectables or families block to define your world. For more details, refer to the ReadMe
    • NEW: the KMP setup was updated to a proper multiplatform gradle setup. That way it is now possible to add Fleks to a KorGE dependency via following line in the korge block
      dependencyMulti("io.github.quillraven.fleks:Fleks:1.3-KMP", registerPlugin = false)
      
    Source code(tar.gz)
    Source code(zip)
  • 1.3-JVM(Jun 2, 2022)

    Another update within a short period of time because the last version introduced a bug. I should not work an releases ~midnight time - note to myself for the future! :)

    Anyway, here is the changelog:

    • BUGFIX: ComponentListener did not get notified when entities are created in a system's init block
    • BUGFIX: nested Family iteration was not properly handled when entities got removed during iteration. The entity removal delay was removed by the inner loop instead of the outer
    • NEW: users have now access to the family of an IteratingSystem
    • NEW: family now also contains a configureEntity function which allows users to modify an entity outside of a system in a convenient way
    • NEW: a new FamilyListener support is added which allows users to react on when an entity is added to, or removed from a family

    ReadMe is updated as well. You can find examples for the new Family and FamilyListener functionality at the end before the Performance section.

    Enjoy!

    Source code(tar.gz)
    Source code(zip)
  • 1.2-JVM(May 30, 2022)

    This version contains some new functionality to cover new use cases that were not nicely supported before:

    • NEW: make world a default dependency for the dependency injection mechanism. That way it is now possible to easily pass the world to a ComponentListener
    • NEW: add getter for world's systems
    • NEW: add configureEntity to world. This makes it now possible to modify an entity outside of a system as requested in the comments section of my YouTube channel
    • NEW: add support to create families out of a system via the world. This is the biggest change of this version and adds new flexibility on how to handle and iterate over entities with specific component configurations
    • UPDATE: change dokka from 1.6.10 to 1.6.21
    Source code(tar.gz)
    Source code(zip)
  • 1.1-JVM(May 12, 2022)

    A small bugfix release that fixes the issue that entities that are created in a system's constructor are not added correctly to families. EntityListener did not get notified correctly in this specific case.

    Source code(tar.gz)
    Source code(zip)
  • 1.0-JVM(Apr 28, 2022)

    Minor update to the JVM version of Fleks:

    • Update gradle from 7.3.3 to 7.4.2
    • Update Kotlin from 1.6.10 to 1.6.21
    • add new getOrNull function to ComponentMapper
    • refactor system initialization. onInit is now no longer necessary. The world can be accessed in the normal init constructor block of a system
    • update ReadMe to better explain the two different flavors of Fleks and why they are there (KMP and JVM)

    With the new getOrNull function it is now possible to write concise code together with Kotlin's let like:

    val animations : ComponentMapper<Animation>
    // ....
    animations.getOrNull(entity)?.let { animation ->
      animation.loop = false
    }
    Source code(tar.gz)
    Source code(zip)
  • 1.0-KMP-RC1(Feb 28, 2022)

  • 1.0-RC3(Jan 30, 2022)

    Most likely the last RC for version 1.0 including two more utility functions:

    • NEW: addOrUpdate function for configureEntity to easily add or update a component (#29)
    • NEW: forEach function for the world to iterate over all active entities (#30)
    Source code(tar.gz)
    Source code(zip)
  • 1.0-RC2(Jan 19, 2022)

    • NEW: named dependencies - it is now possible to add multiple dependencies of the same type using the new @Qualifier annotation
    • NEW: onInit function for systems to initialize logic that is dependent on the world. Also, accessing the world in a normal init block will now throw an Exception
    • NEW: ComponentMapper can now be retrieved from anywhere via the world with the new mapper function
    • BUGFIX: Removing the same entity multiple times will no longer add it multiple times to the recycled list
    • BUGFIX: removeAll will no longer clean up delayed entity removals. This is now done at the end of the iteration of the current system as it is done with normal removal
    Source code(tar.gz)
    Source code(zip)
  • 1.0-RC1(Jan 2, 2022)

    • UPDATE: update gradle to 7.3.3 and Kotlin to 1.6.10 (log4j vulnerability fixes)
    • NEW: dispose function for world to remove all entities and dispose systems via a new onDispose function
    • NEW: ComponentListener for WorldConfiguration to add custom code for add/remove of specific components
    • NEW: entity is now directly available in the configuration block of a newly created entity
    • BUGFIX: families are now correctly updated in systems using a Fixed interval
    • NEW: a new exception is now thrown when injectables are not used during system creation
    Source code(tar.gz)
    Source code(zip)
  • 1.0-alpha(Nov 28, 2021)

  • Pre-Release-20211120(Nov 20, 2021)

    First Pre-Release version containing the entire functionality and documentation. Will try to add a normal mavenCentral publish later. For now, please manually download the jar and include it as a dependency to your project.

    Update: artifact is now available on mavenCentral:

    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation("io.github.quillraven.fleks:Fleks:preRelease-20211120")
    }
    
    Source code(tar.gz)
    Source code(zip)
    Fleks-preRelease-20211118-javadoc.jar(414.98 KB)
    Fleks-preRelease-20211118-sources.jar(15.99 KB)
    Fleks-preRelease-20211118.jar(73.25 KB)
Owner
Simon
Simon
Minecraft 1.18.2 Backport of Petal, a performance-oriented fork of Purpur intended to increase performance for entity-heavy servers by implementing multi-threaded and asynchronous improvements.

Sakura Performance Minecraft JAR Sakura is a performance-oriented fork of Purpur intended to increase performance for entity-heavy servers by implemen

etil.sol 14 Nov 23, 2022
A fast-prototyping command line system

Kotlin-er CLI A different take on making command line programs, emphasizing development speed over customization Status Auto-deployed to Maven Central

Lightning Kite 22 Jan 22, 2022
Clickstream - A Modern, Fast, and Lightweight Android Library Ingestion Platform.

Clickstream is an event agnostic, real-time data ingestion platform. Clickstream allows apps to maintain a long-running connection to send data in real-time.

Gojek 60 Dec 24, 2022
Celebrate more with this lightweight confetti particle system 🎊

Konfetti ?? ?? Celebrate more with this lightweight confetti particle system. Create realistic confetti by implementing this easy to use library. Demo

Dion Segijn 2.7k Dec 28, 2022
Spigot-Plugin message providing system written in Kotlin

teller Spigot-Plugin message providing system written in Kotlin Usage Create an instance of PropertiesMessageProvider using the Constructor with an in

Luca Zieserl 2 Jan 16, 2022
A lightweight cache library written in Kotlin

[NEW] Released to Maven Central: 'com.github.yundom:kache:1.x.x' Kache A runtime in-memory cache. Installation Put this in your build.gradle implemen

Dennis 22 Nov 19, 2022
🔓 Kotlin version of the popular google/easypermissions wrapper library to simplify basic system permissions logic on Android M or higher.

EasyPermissions-ktx Kotlin version of the popular googlesample/easypermissions wrapper library to simplify basic system permissions logic on Android M

Madalin Valceleanu 326 Dec 23, 2022
[Android Library] Get easy access to device information super fast, real quick

DeviceInfo-Sample Simple, single class wrapper to get device information from an android device. This library provides an easy way to access all the d

Anitaa Murthy 193 Nov 20, 2022
Clay is an Android library project that provides image trimming which is originally an UI component of LINE Creators Studio

Clay Clay is an Android library project that provides image trimming. Fully written in Kotlin, Clay is originally a UI component of LINE Creators Stud

LINE 119 Dec 27, 2022
Jetpack Compose, Kotlin, MVVM, Navigation Component, Hilt, Retrofit2

Jetpack-Compose-Blueprint Jetpack Compose, Kotlin, MVVM, Navigation Component, Hilt, Retrofit2 Apps Packages data : It contains all the data accessing

Jai Khambhayta 14 Dec 15, 2022
FaceTimeClone app that implements Coroutines , mvvm architecture , clean architecture , navigation component , hilt , etc.... using kotlin language

This repository contains a FaceTimeClone app that implements Coroutines , mvvm architecture , clean architecture , navigation component , hilt , etc.... using kotlin language

null 17 Dec 13, 2022
🚧 General-Purpose Module System for Kotlin.

?? Modules: General-Purpose Module System A module system & loader for Kotlin. Made for me to use. Architecture Module is a building block for this sy

lhwdev 0 Dec 29, 2021
Kotlin DALL·E 2 is a new AI system that can create realistic images and art from a description in natural language.

OpenAI Dall•E AI Kotlin Mobile App OpenAI Dall•E Application Build With Kotlin MVVM (Model - View - ViewModel) Clean Architecture, Beautiful Design UI

Murat ÖZTÜRK 15 Jan 1, 2023
Fast Seek for ExoPlayer

FastExoPlayerSeeker Introduction Adds fast seeking for exoplayer. (Note: it also depends on the amount of video encoding, mainly IDR Frames) How to in

Jan Rabe 40 Dec 27, 2022
Feature flags solution that is fast, lean, and open-source.

FFS Feature flags solution that is fast, lean, and open-source. Documentation Full documentation available at https://doist.github.io/ffs/. Project FF

Doist 84 Oct 31, 2022
Koin Annotations - help declare Koin definition in a very fast and intuitive way, and generate all underlying Koin DSL for you

The goal of Koin Annotations project is to help declare Koin definition in a very fast and intuitive way, and generate all underlying Koin DSL for you. The goal is to help developer experience to scale and go fast ?? , thanks to Kotlin Compilers.

insert-koin.io 68 Jan 6, 2023
Simple, fast, efficient logging facade for Android apps

µlog Simple, fast, and efficient logging facade for Android apps. Inspired by Timber and Logcat. Features Lazy message evaluation Pluggable backends (

Danny Lin 9 Oct 21, 2022
Easy lightweight SharedPreferences library for Android in Kotlin using delegated properties

Easy lightweight SharedPreferences library for Android in Kotlin using delegated properties Idea Delegated properties in Kotlin allow you to execute a

null 25 Dec 27, 2022
Koi, a lightweight kotlin library for Android Development.

Koi - A lightweight Kotlin library for Android Koi include many useful extensions and functions, they can help reducing the boilerplate code in Androi

Hello World 514 Nov 29, 2022