libktx / ktx

Kotlin extensions for the libGDX game framework
https://libktx.github.io/
Creative Commons Zero v1.0 Universal
1.36k stars 74 forks source link

Problem with inline function Engine.add in ktx-ashley #157

Closed Divelix closed 6 years ago

Divelix commented 6 years ago

I just started to exlore ktx, and stuck on Ashley module. When I add Entity to an Engine "traditional way" everything works fine:

        val entity = Entity()
        val dc = DrawableComponent(Sprite(Texture("image1.png")))
        val tc = TransformComponent(Vector2(50f, 50f))
        entity.add(dc)
        entity.add(tc)
        engine.addEntity(entity)

But when I try to do the same in "ktx way" compiler throws an error:

    engine.add {
        entity {
            with<DrawableComponent> {
                sprite = Sprite(Texture("image1.png"))
            }
            with<TransformComponent> {
                position.set(50f, 50f)
            }
        }
    }

Here is the error:

Exception in thread "LWJGL Application" java.lang.IllegalStateException: createComponent(T::class.java).apply(configure) must not be null

P.S. it complains on engine.add line

czyzby commented 6 years ago

@Jkly

czyzby commented 6 years ago

I cannot reproduce the issue and unit tests cover a similar case. Which Kotlin version are you using? Would you mind posting the component classes and Engine initiation code?

czyzby commented 6 years ago

Just so you know, you can use engine.entity without the add block. add is very similar to Kotlin's apply. This should be an equivalent:

        engine.entity {
            with<DrawableComponent> {
                sprite = Sprite(Texture("image1.png"))
            }
            with<TransformComponent> {
                position.set(50f, 50f)
            }
        }

ktx-ashley also uses Ashley 1.7.3 - make sure your dependencies are compatible.

Divelix commented 6 years ago

Here is the full code:

class AshleyScreen(game: Main): KtxScreen {

    private val engine = Engine()

    override fun show() {
        engine.add {
            entity {
                with<DrawableComponent> {
                    sprite = Sprite(Texture("image1.png"))
                }
                with<TransformComponent> {
                    position.set(50f, 50f)
                }
            }
        }
        engine.addSystem(RenderSystem())
        engine.addSystem(MovingSystem())
    }

    override fun render(delta: Float) {
        clearScreen(0.8f, 0.8f, 0.8f)
        engine.update(delta)
    }
}

// Components
class DrawableComponent(var sprite: Sprite): Component 
class TransformComponent(val position: Vector2 = Vector2()): Component

//Systems
class RenderSystem: IteratingSystem(allOf(TransformComponent::class, DrawableComponent::class).get()) {

    val batch = SpriteBatch()

    val tansformComponent = ComponentMapper.getFor(TransformComponent::class.java)
    val drawableComponent = ComponentMapper.getFor(DrawableComponent::class.java)

    override fun processEntity(entity: Entity, deltaTime: Float) {
        val t = tansformComponent[entity]
        val d = drawableComponent[entity]
        d.sprite.setPosition(t.position.x, t.position.y)

        batch.begin()
        d.sprite.draw(batch)
        batch.end()
    }
}

class MovingSystem: IteratingSystem(allOf(TransformComponent::class).get()) {

    val transformComponent = ComponentMapper.getFor(TransformComponent::class.java)

    override fun processEntity(entity: Entity, deltaTime: Float) {
        val t = transformComponent[entity]

        t.position.y += 1f
    }
}

I use Ashley 1.7.3, kotlin 1.2.61 and option with engine.entity throws same exception

Jkly commented 6 years ago

@Divelix It's probably because createComponent is returning null because you don't have a no-arg constructor for DrawableComponent and TransformComponent.

The code for createComponent returns null if it can't create the component via reflection:

public <T extends Component> T createComponent (Class<T> componentType) {
    try {
        return ClassReflection.newInstance(componentType);
     } catch (ReflectionException e) {
         return null;
     }
}
Jkly commented 6 years ago

I think arguably ktx-ashley should throw a more descriptive error if createComponent returns null, something like "Could not instantiate component $type - check the component has a no-arg constructor"

Divelix commented 6 years ago

Oh, ok, I just added default values for both components and it works fine now:

class DrawableComponent(var sprite: Sprite = Sprite(Texture("image1.png"))): Component 
class TransformComponent(val position: Vector2 = Vector2()): Component

It was really unobvious thing for me, thank you.

czyzby commented 6 years ago

@Jkly Do you want me to add the null check or will you submit a PR?

Jkly commented 6 years ago

@czyzby if you have time to do it now go for it! Otherwise I will create a PR later today.

czyzby commented 6 years ago

Don't mind if you take this one. ;)

czyzby commented 6 years ago

@Divelix Just so you know, Sprite has a no-argument constructor that you can use in your case. You should also avoid creating Texture objects on demand - they reload the image every time, which might quickly become an issue if you don't cache and reuse Texture objects.

class DrawableComponent(var sprite: Sprite = Sprite()): Component 
czyzby commented 6 years ago

This is already addressed on the development branch and should be available soon in the snapshot release.

czyzby commented 6 years ago

@Divelix As of 1.9.8-b5, an exception with a meaningful message is thrown if you're missing a no-arg constructor. It should be available via Maven Central very soon.