Wednesday, March 23, 2016

Look, Ma, No Framework! (my first Kotlin project)

I like the way that an entity component system (ecs) organizes my code. But I don't always want to use a big framework. Functional languages such as Kotlin can give me much of the benefits of ecs without the overhead.

Let's start with components:

data class Bounds(val radius: Float)
data class Expires(val value: Float)
data class Health(val current: Int, val maximum: Int)
data class Position(val x: Float, val y: Float)
data class Resource(val path: String)
data class Scale(val x: Float, val y:Float)
data class Size(val w: Float, val h:Float)
data class Sprite(val texture: TextureRegion)
data class Tween(val min:Float, val max:Float, val speed:Float, val repeat:Boolean, val active:Boolean)
data class Velocity(val x: Float, val y: Float)

That's it. TextureRegion is defined in LibGDX, and I defined a couple of enums - EntityType and SpriteLayer (https://github.com/darkoverlordofdata/shmupwarz-libgdx-kotlin) so that we can define our entity:




data class Entity(
    val id:         Int,           /* Unique id */
    val name:       String,        /* Display name */
    val active:     Boolean,       /* In use? */
    val entityType: EntityType,    /* EntityType Enum */
    val layer:      SpriteLayer,   /* Display Layer Enum 
         /* C O M P O N E N T S * */
    val bounds:     Bounds?,       /* Radius */
    val expires:    Expires?,      /* Entity expiration timer */
    val health:     Health?,       /* Health counter */
    val position:   Position?,     /* Screen x,y */
    val resource:   Resource?,     /* Sprite asset */
    val scale:      Scale?,        /* Size scale */
    val size:       Size?,         /* Display size */
    val sprite:     Sprite?,       /* Texture */
    val tween:      Tween?,        /* Tweener */
    val velocity:   Velocity?    /* Speed x,y */
)
var uniqueId: Int = 0

fun createEntity(entityType: EntityType, layer : SpriteLayer, name : String):Entity {
    uniqueId += 1
    return Entity(uniqueId, name, false, entityType, layer, null, null, null, null, null, null, null, null, null, null)
}



Components are initialized to null. I use a factory functions to create a full entities, such as Player:


fun createPlayer(entity:Entity, width: Int, height: Int):Entity {
    val name = entity.name
    val path = "images/$name.png"
    return entity.copy(
        active    = true,
        bounds    = Bounds(43.0f),
        health    = Health(100, 100),
        resource  = Resource(path),
        position  = Position(width.toFloat()/2.0f, 100.0f),
        sprite    = Sprite(TextureRegion(Texture(Gdx.files.internal(path)))),
        velocity  = Velocity(0.0f, 0.0f)
    )
}


Systems use pattern matching to determine which entities they act upon.


fun movementSystem(entity: Entity, delta: Float): Entity =
    when {
        (entity.position != null
        && entity.velocity != null) -> {

            val position = entity.position
            val velocity = entity.velocity
            val x = position.x + velocity.x * delta
            val y = position.y + velocity.y * delta
            entity.copy(position = Position(x, y))
        }
        else -> entity
    }


From a functional viewpoint, Kotlin's implementation of pattern matching seems a bit lame. Better destucturing would help. I should be able to say:

val (position, velocity) = entity

But I can't do that either, Kotlin doesn't support adhoc destructuring. Instead I would need to use

val (id, name, active, entityType, layer, bounds, expires, health, position, resource, scale, size, sprite, tween, velocity) = entity

Who thought that was a good idea? You can see why I'm not using it. Oh well. The thing to remember is that pattern matching does work. I think that compared to all of the good features of Kotlin, this is something I can live with, and hope that JetBrains will improve it - after all, this is only version 1.0.1!

Those are the pieces. How do they fit together? There is a joke-
Q How many Haskell programmers does it take to change a lightbulb
A None. They build a new house around the new lightbulb.

Well, that's what we're going to do, only in Kotlin. Notice that all of the fields of our components and entities are immutable (val). We cannot change them, only create new ones. Notice in the movement system, where the function returns the original entity or with entity.copy(position=Position(x,y)).

First, we create an array of all our entities, active or not:

fun createLevel(): List = arrayListOf(
    createEntity(EntityType.Enemy, SpriteLayer.ENEMY1, "enemy1"),
    ...
    createEntity(EntityType.Bullet, SpriteLayer.BULLET, "bullet"),
    createEntity(EntityType.Bullet, SpriteLayer.BULLET, "bullet"),
    createEntity(EntityType.Bullet, SpriteLayer.BULLET, "bullet"),
    ...
    createEntity(EntityType.Player, SpriteLayer.PLAYER, "fighter"),
    createEntity(EntityType.Explosion, SpriteLayer.EXPLOSION, "explosion"),
    ...
    )


Then our main update loop will create a pipeline out of our systems. This is where it all happens. Once each frame, we recalculate our game state, and then display it:


fun update(delta: Float, mainBatch: SpriteBatch) {
    level = collisionSystem(
        enemySpawningSystem(level, delta)
            .map {inputSystem(it, delta) }
            .map {entitySystem(it, delta) }
            .map {movementSystem(it, delta) }
            .map {tweenSystem(it, delta) }
            .map {expiringSystem(it, delta) }
            .map {removeOffscreenSystem(it, delta) }, delta)

    level.filter { it.active }
        .sortedBy { it.layer }
        .map {spriteRenderSystem(it, mainBatch)}

}


You might be tempted to just add your active entities as needed and remove them when you are done. I tried that. After about a minute, the game will grind to a halt due to memory fragmentation.

I've uploaded the full working project in AndroidStudio to github. It should run on both desktop and android. I generated the ios version also, but I am unable to test it. 

No comments:

Post a Comment