Starter Kits.json

A simple Hello World

An example showing how to combine a KorGE client, with a Ktor server having a module with shared code.

Shows how to use the Compose Multiplatform runtime with KorGE.

Shows how to use DragonBones, the Korge Mascots, the VirtualController and LDtk maps to create a Platformer game.

Playable here

Shows how to use Tiled maps & .ASE image format for characters

A simple Hello World using KorGE Fleks

Showcases.json

A simple 2048 game implemented via KorGE. You can play the game here. Tutorials for creating this in the blog.

A simple chess game implemented via KorGE. There is a Video Tutorial explaining it in YouTube. You can play it here.

A Pang! clone initially written in Div Game Studio by Jose Manuel Nabas Millan and ported to KorGE by hyperbou. Updated to KorGE 4.0.0. Playable here

Xeno Tactic is a tower defense game where you must not let your opponents walk across your base. Build some walls and towers to defend your base.

GFX.json

Wizard Boy & Girl + Orcs.

Animations: idle, gesture, walk, attack, death

Wizard Male

Download wizard_m 👁️

Wizard Female

Download wizard_f 👁️

Cleric Male

Download cleric_m 👁️

Cleric Female

Download cleric_f 👁️

Ranger Male

Download ranger_m 👁️

Ranger Female

Download ranger_f 👁️

Rogue Male

Download rogue_m 👁️

Rogue Female

Download rogue_f 👁️

Warrior Male

Download warrior_m 👁️

Warrior Female

Download warrior_f 👁️

Bat

Download bat 👁️

Rat

Download rat 👁️

Skeleton

Download skeleton 👁️

Snake

Download snake 👁️

Minotaur

Download minotaur 👁️

Orc1

Download orc1 👁️

Orc2

Download orc2 👁️

Goblin1

Download goblin1 👁️

Goblin2

Download goblin2 👁️

Slime Blue

Download slime_blue 👁️

Slime Green

Download slime_green 👁️

Slime Red

Download slime_red 👁️

Slime Yellow

Download slime_yellow 👁️

Usage code

val atlas = MutableAtlasUnit()
val wizardMale = KR.gfx.wizardM.__file.readImageDataContainer(ASE.toProps(), atlas)
val wizardFemale = KR.gfx.wizardF.__file.readImageDataContainer(ASE.toProps(), atlas)

imageDataView(wizardMale.default).scale(4, 4).also { it.smoothing = false }.also { view ->
    println(view.animationNames)
    view.animation = "idle"
    view.play()
}

imageDataView(wizardFemale.default).scale(4, 4).also { it.smoothing = false }.also { view ->
    println(view.animationNames)
    view.animation = "gesture"
    view.play()
    view.x = 128f
}

Dungeon Tileset

Download dungeon_tileset_calciumtrice 👁️

LDtk Base Map

Download dungeon_tilesmap_calciumtrice 👁️

Required the LDtk extension.

CC0 VFX made with SpriteMancer by CodeManu:

Effect_Anima.gif

Download Effect_Anima.gif 👁️

Effect_BigHit.gif

Download Effect_BigHit.gif 👁️

Effect_BloodImpact.gif

Download Effect_BloodImpact.gif 👁️

Effect_Charged.gif

Download Effect_Charged.gif 👁️

Effect_Constellation.gif

Download Effect_Constellation.gif 👁️

Effect_DitheredFire.gif

Download Effect_DitheredFire.gif 👁️

Effect_EldenRing.gif

Download Effect_EldenRing.gif 👁️

Effect_ElectricShield.gif

Download Effect_ElectricShield.gif 👁️

Effect_Explosion.gif

Download Effect_Explosion.gif 👁️

Effect_Explosion2.gif

Download Effect_Explosion2.gif 👁️

Effect_Explosion2_4x.gif

Download Effect_Explosion2_4x.gif 👁️

Effect_Explosion_4x.gif

Download Effect_Explosion_4x.gif 👁️

Effect_FastPixelFire.gif

Download Effect_FastPixelFire.gif 👁️

Effect_Hyperspeed.gif

Download Effect_Hyperspeed.gif 👁️

Effect_Hyperspeed_4x.gif

Download Effect_Hyperspeed_4x.gif 👁️

Effect_Impact.gif

Download Effect_Impact.gif 👁️

Effect_Impact_2x.gif

Download Effect_Impact_2x.gif 👁️

Effect_Kabooms.gif

Download Effect_Kabooms.gif 👁️

Effect_Kabooms_2x.gif

Download Effect_Kabooms_2x.gif 👁️

Effect_Magma.gif

Download Effect_Magma.gif 👁️

Effect_PowerChords.gif

Download Effect_PowerChords.gif 👁️

Effect_PuffAndStars.gif

Download Effect_PuffAndStars.gif 👁️

Effect_SmallHit.gif

Download Effect_SmallHit.gif 👁️

Effect_Tentacles.gif

Download Effect_Tentacles.gif 👁️

Effect_TheVortex.gif

Download Effect_TheVortex.gif 👁️

Effect_Wheel.gif

Download Effect_Wheel.gif 👁️

Effect_Worm.gif

Download Effect_Worm.gif 👁️

Variant with shadows

Download chess_shadow.atlas 👁️

Variant without shadows

Download chess.atlas 👁️

Explosions

Download explosions 👁️

val animation = SpriteAnimation(
    resourcesVfs["gfx/exp2.jpg"].readBitmapSlice().splitInRows(64, 64),
    60.milliseconds
)

val random = Random(0L)
interval(0.02.seconds) {
    sprite(animation).xy(random[0, 250], random[0, 250]).also { it.blendMode = BlendMode.SCREEN }
        .also { sprite -> sprite.onAnimationCompleted { sprite.removeFromParent() } }
        .playAnimation()
}

+ Sound

Download sound

val sound = resourcesVfs["sfx/explodify.mp3"].readSound()
val animation = SpriteAnimation(
    resourcesVfs["gfx/exp2.jpg"].readBitmapSlice().splitInRows(64, 64),
    60.milliseconds
)

val random = Random(0L)
interval(.25.seconds) {
    sound.playNoCancel(1.playbackTimes).also { it.volume = .3 }
    sprite(animation).xy(random[0, 250], random[0, 250]).also { it.blendMode = BlendMode.SCREEN }
        .also { sprite -> sprite.onAnimationCompleted { sprite.removeFromParent() } }
        .playAnimation()
}

LICENSE CC0: Created by https://kenney.nl/

Download icons.atlas 👁️

Untyped usage:

val icons = resourcesVfs["gfx/icons.atlas.json"].readAtlas()
image(icons["arrowDown.png"])

Typed usage:

You can also have type-safe access to the assets like this:

val icons = IconsAtlas(resourcesVfs["gfx/icons.atlas.json"].readAtlas())
image(icons.arrowDown)

Add this inline class for typed access:

inline class IconsAtlas(val atlas: Atlas) {
    val arrowDown get() = atlas["arrowDown.png"]
    val arrowLeft get() = atlas["arrowLeft.png"]
    val arrowRight get() = atlas["arrowRight.png"]
    val arrowUp get() = atlas["arrowUp.png"]
    val audioOff get() = atlas["audioOff.png"]
    val audioOn get() = atlas["audioOn.png"]
    val barsHorizontal get() = atlas["barsHorizontal.png"]
    val barsVertical get() = atlas["barsVertical.png"]
    val button1 get() = atlas["button1.png"]
    val button2 get() = atlas["button2.png"]
    val button3 get() = atlas["button3.png"]
    val buttonA get() = atlas["buttonA.png"]
    val buttonB get() = atlas["buttonB.png"]
    val buttonL get() = atlas["buttonL.png"]
    val buttonL1 get() = atlas["buttonL1.png"]
    val buttonL2 get() = atlas["buttonL2.png"]
    val buttonR get() = atlas["buttonR.png"]
    val buttonR1 get() = atlas["buttonR1.png"]
    val buttonR2 get() = atlas["buttonR2.png"]
    val buttonSelect get() = atlas["buttonSelect.png"]
    val buttonStart get() = atlas["buttonStart.png"]
    val buttonX get() = atlas["buttonX.png"]
    val buttonY get() = atlas["buttonY.png"]
    val checkmark get() = atlas["checkmark.png"]
    val contrast get() = atlas["contrast.png"]
    val cross get() = atlas["cross.png"]
    val down get() = atlas["down.png"]
    val downLeft get() = atlas["downLeft.png"]
    val downRight get() = atlas["downRight.png"]
    val exclamation get() = atlas["exclamation.png"]
    val exit get() = atlas["exit.png"]
    val exitLeft get() = atlas["exitLeft.png"]
    val exitRight get() = atlas["exitRight.png"]
    val export get() = atlas["export.png"]
    val fastForward get() = atlas["fastForward.png"]
    val gamepad get() = atlas["gamepad.png"]
    val gamepad1 get() = atlas["gamepad1.png"]
    val gamepad2 get() = atlas["gamepad2.png"]
    val gamepad3 get() = atlas["gamepad3.png"]
    val gamepad4 get() = atlas["gamepad4.png"]
    val gear get() = atlas["gear.png"]
    val home get() = atlas["home.png"]
    val import get() = atlas["import.png"]
    val information get() = atlas["information.png"]
    val joystick get() = atlas["joystick.png"]
    val joystickLeft get() = atlas["joystickLeft.png"]
    val joystickRight get() = atlas["joystickRight.png"]
    val joystickUp get() = atlas["joystickUp.png"]
    val larger get() = atlas["larger.png"]
    val leaderboardsComplex get() = atlas["leaderboardsComplex.png"]
    val leaderboardsSimple get() = atlas["leaderboardsSimple.png"]
    val left get() = atlas["left.png"]
    val locked get() = atlas["locked.png"]
    val massiveMultiplayer get() = atlas["massiveMultiplayer.png"]
    val medal1 get() = atlas["medal1.png"]
    val medal2 get() = atlas["medal2.png"]
    val menuGrid get() = atlas["menuGrid.png"]
    val menuList get() = atlas["menuList.png"]
    val minus get() = atlas["minus.png"]
    val mouse get() = atlas["mouse.png"]
    val movie get() = atlas["movie.png"]
    val multiplayer get() = atlas["multiplayer.png"]
    val musicOff get() = atlas["musicOff.png"]
    val musicOn get() = atlas["musicOn.png"]
    val next get() = atlas["next.png"]
    val open get() = atlas["open.png"]
    val pause get() = atlas["pause.png"]
    val phone get() = atlas["phone.png"]
    val plus get() = atlas["plus.png"]
    val power get() = atlas["power.png"]
    val previous get() = atlas["previous.png"]
    val question get() = atlas["question.png"]
    val `return` get() = atlas["return.png"]
    val rewind get() = atlas["rewind.png"]
    val right get() = atlas["right.png"]
    val save get() = atlas["save.png"]
    val scrollHorizontal get() = atlas["scrollHorizontal.png"]
    val scrollVertical get() = atlas["scrollVertical.png"]
    val share1 get() = atlas["share1.png"]
    val share2 get() = atlas["share2.png"]
    val shoppingBasket get() = atlas["shoppingBasket.png"]
    val shoppingCart get() = atlas["shoppingCart.png"]
    val siganl1 get() = atlas["siganl1.png"]
    val signal2 get() = atlas["signal2.png"]
    val signal3 get() = atlas["signal3.png"]
    val singleplayer get() = atlas["singleplayer.png"]
    val smaller get() = atlas["smaller.png"]
    val star get() = atlas["star.png"]
    val stop get() = atlas["stop.png"]
    val tablet get() = atlas["tablet.png"]
    val target get() = atlas["target.png"]
    val trashcan get() = atlas["trashcan.png"]
    val trashcanOpen get() = atlas["trashcanOpen.png"]
    val trophy get() = atlas["trophy.png"]
    val unlocked get() = atlas["unlocked.png"]
    val up get() = atlas["up.png"]
    val upLeft get() = atlas["upLeft.png"]
    val upRight get() = atlas["upRight.png"]
    val video get() = atlas["video.png"]
    val warning get() = atlas["warning.png"]
    val wrench get() = atlas["wrench.png"]
    val zoom get() = atlas["zoom.png"]
    val zoomDefault get() = atlas["zoomDefault.png"]
    val zoomIn get() = atlas["zoomIn.png"]
    val zoomOut get() = atlas["zoomOut.png"]
}

SFX.json

https://opengameart.org/content/100-cc0-sfx

val sound = resourcesVfs["sfx/file.mp3"].readSound()

You will have to install first MOD/S3M/XM support

Files from https://modarchive.org/index.php?request=view_by_license&query=cc0

defaultAudioFormats.register(MOD, S3M, XM)
val music = resourcesVfs["sfx/file.mod"].readMusic()

or

val music = resourcesVfs["sfx/file.mod"].readMusic(AudioDecodingProps.DEFAULT.copy(formats = MOD))

Fonts.json

Coconut
👁️

Jupiteroid
👁️

PublicPixel
👁️

Libraries & Modules.json

Adds initial experimental 3D.

Download crate 👁️

Download korge 👁️

import korlibs.image.bitmap.*
import korlibs.image.color.*
import korlibs.image.format.readNativeImage
import korlibs.io.async.launchImmediately
import korlibs.io.file.std.resourcesVfs
import korlibs.korge.KeepOnReload
import korlibs.korge.scene.Scene
import korlibs.korge.tween.tween
import korlibs.korge.view.*
import korlibs.korge3d.*
import korlibs.korge3d.shape.*
import korlibs.math.geom.*
import korlibs.time.seconds

class CratesScene : Scene() {
    @KeepOnReload
    var trans = Transform3D()
    @KeepOnReload
    var tick = 0.0

    override suspend fun SContainer.sceneInit() {

        //val korgeTex = KR.korge.read()

        val crateTex = NativeImage(64, 64).context2d {
            fill(Colors.ROSYBROWN) {
                rect(0, 0, 64, 64)
            }
            stroke(Colors.SADDLEBROWN, 8f) {
                rect(0, 0, 63, 63)
                line(Point(0, 0), Point(63, 63))
            }
        }

        //val crateTex = KR.crate.read().mipmaps(true)
        //val crateTex = KR.dice.__file.readBitmap(QOI).mipmaps(true)
        val crateMaterial = Material3D(diffuse = Material3D.LightTexture(crateTex))

        solidRect(512, 512, MaterialColors.AMBER_200).alpha(0.5)
        //image(korgeTex).alpha(0.5)

        scene3D {
            //camera.set(fov = 60.degrees, near = 0.3, far = 1000.0)

            //light().position(0, 0, -3)

            //polyline3d { }
            polyline3D(Colors.BLUEVIOLET) {
                moveTo(Vector3(-10f, 0f, 0f))
                lineTo(Vector3(10f, 0f, 0f))
            }
            polyline3D(Colors.MEDIUMVIOLETRED) {
                moveTo(Vector3(0f, -10f, 0f))
                lineTo(Vector3(0f, 10f, 0f))
            }
            polyline3D(Colors["#8cb04d"]) {
                moveTo(Vector3(0f, 0f, -10f))
                lineTo(Vector3(0f, 0f, 10f))
            }
            polyline3D(Colors.WHITE) {
                moveTo(Vector3(0f, 0f, 0f))
                lineTo(Vector3(2f, 0f, 0f))
                moveTo(Vector3(0f, 0f, 0f))
                lineTo(Vector3(0f, 2f, 0f))
                moveTo(Vector3(0f, 0f, 0f))
                lineTo(Vector3(0f, 0f, 2f))
            }
            val cube1 = cube().material(crateMaterial)
            sphere(1f).position(1, 0, 0).material(crateMaterial)
            torus(1f).position(-1, 0, 0).material(crateMaterial)
            cone(1f).position(0, -1, 0).material(crateMaterial)
            cylinder(1f).position(0, -2, 0).material(crateMaterial)
            //cube(2.0, 2.0)
            val cube2 = cube().position(0, 2, 0).scale(1, 2, 1).rotation(0.degrees, 0.degrees, 45.degrees).material(crateMaterial)
            val cube3 = cube().position(-5, 0, 0).material(crateMaterial)
            val cube4 = cube().position(+5, 0, 0).material(crateMaterial)
            val cube5 = cube().position(0, -5, 0).material(crateMaterial)
            val cube6 = cube().position(0, +5, 0).material(crateMaterial)
            val cube7 = cube().position(0, 0, -5).material(crateMaterial)
            val cube8 = cube().position(0, 0, +5).material(crateMaterial)

            addUpdater {
                val angle = (tick / 4.0).degrees
                trans.setTranslationAndLookAt(
                    cos(angle * 2) * 4, cos(angle * 3) * 4, -sin(angle) * 4, // Orbiting camera
                    0f, 1f, 0f
                )
                camera.transform.copyFrom(trans)
                camera.invalidateRender()
                tick += it.milliseconds / 16.0
            }

            launchImmediately {
                while (true) {
                    tween(time = 16.seconds) {
                        cube1.modelMat.identity().rotate((it * 360).degrees, 0.degrees, 0.degrees)
                        cube2.modelMat.identity().rotate(0.degrees, (it * 360).degrees, 0.degrees)
                    }
                }
            }
        }

        solidRect(512, 512, Colors.BLUEVIOLET).position(views.virtualWidth, 0).anchor(1, 0).alpha(0.5)
        //image(korgeTex).position(views.virtualWidth, 0).anchor(1, 0).alpha(0.5)
    }
}
val config = SWFExportConfig(
    rasterizerMethod = ShapeRasterizerMethod.NONE,
    generateTextures = false,
    graphicsRenderer = GraphicsRenderer.SYSTEM,
)

container {
    this += resourcesVfs["morph.swf"].readSWF(views, config, false).createMainTimeLine()
    this += resourcesVfs["dog.swf"].readSWF(views, config, false).createMainTimeLine()
    this += resourcesVfs["test1.swf"].readSWF(views, config, false).createMainTimeLine().position(400, 0)
    this += resourcesVfs["demo3.swf"].readSWF(views, config, false).createMainTimeLine()
}

Includes a Vampire and a Vamp spritesheet in .ASE format and code to load it.

Sprites from: https://finalbossblues.com/timefantasy/freebies/evil-transforming-vampires/

val atlas = MutableAtlasUnit(2048, 2048)
val characters = EvilTransformingVampires.readImages(atlas)
val player = imageDataView(characters.vampire, EvilTransformingVampires.Animations.DOWN, playing = true, smoothing = false)
    .scale(4, 4)
    .xy(120, 120)

fun update() {
    val mx = if (input.keys[Key.LEFT]) -1 else if (input.keys[Key.RIGHT]) +1 else 0
    val my = if (input.keys[Key.UP]) -1 else if (input.keys[Key.DOWN]) +1 else 0
    if (mx == 0 && my == 0) player.stop() else player.play()
    when {
        mx < 0 -> player.animation = EvilTransformingVampires.Animations.LEFT
        mx > 0 -> player.animation = EvilTransformingVampires.Animations.RIGHT
        my < 0 -> player.animation = EvilTransformingVampires.Animations.UP
        my > 0 -> player.animation = EvilTransformingVampires.Animations.DOWN
    }
}

addUpdater { update() }
keys {
    downFrame(Key.LEFT, 16.milliseconds) { player.x -= 4 }
    downFrame(Key.RIGHT, 16.milliseconds) { player.x += 4 }
    downFrame(Key.UP, 16.milliseconds) { player.y -= 4 }
    downFrame(Key.DOWN, 16.milliseconds) { player.y += 4 }
}

Jitto

val jitto = JittoView().xy(256, 256).addTo(this)
while (true) {
    jitto.interpolateTo(
        Jitto(
            rightHand = 10.degrees,
            leftHand = 14.degrees,
            leftLeg = +1f,
            rightLeg = -1f,
            leftEyeDist = 0f,
            leftEyeAngle = 0.degrees,
            rightEyeDist = 0f,
            rightEyeAngle = 0.degrees,
            rotation = 45.degrees
        )
    )
    jitto.interpolateTo(
        Jitto(
            rightHand = -10.degrees,
            leftHand = -14.degrees,
            leftLeg = -1f,
            rightLeg = +1f,
        )
    )
}

Load Dragonbones models for KorGE mascots Koral & Gest.

val db = KorgeDbFactory()
db.loadKorgeMascots()

val koral = db.buildArmatureDisplayKoral()
    !!.position(100, 490)
    .scale(SCALE)
    .addTo(this)
    .also { it.animation.play(KorgeMascotsAnimations.IDLE) }

Supports loading FLAC files that have a great compression rate.

https://en.wikipedia.org/wiki/FLAC

Explicitly loading an FLAC sound or music

val data = resourcesVfs["sounds/8Khz-Mono.flac"].readSound(AudioDecodingProps(formats = FLAC))

Getting information (length) of an FLAC file

println(FLAC.tryReadInfo(resourcesVfs["sounds/8Khz-Mono.flac"].open(), AudioDecodingProps(exactTimings = true)))

Registering the FLAC format, so it can detect the format automatically

defaultAudioFormats.register(FLAC)
val data = resourcesVfs["sounds/8Khz-Mono.flac"].readSound()

Support for MOD, XM & S3M music modules.

defaultAudioFormats.register(MOD, S3M, XM)
val music = resourcesVfs["GUITAROU.MOD"].readMusic()
var channel = music.play(times = infinitePlaybackTimes)

You can find some archives in this format here.

Supports loading OPUS files that have a great compression rate.

https://en.wikipedia.org/wiki/Opus_(audio_format)

Explicitly loading an OPUS sound or music

val data = resourcesVfs["sounds/8Khz-Mono.opus"].readSound(AudioDecodingProps(formats = OPUS))

Getting information (length) of an OPUS file

println(OPUS.tryReadInfo(resourcesVfs["sounds/8Khz-Mono.opus"].open(), AudioDecodingProps(exactTimings = true)))

Registering the OPUS format, so it can detect the format automatically

defaultAudioFormats.register(OPUS)
val data = resourcesVfs["sounds/8Khz-Mono.opus"].readSound()

Use KorGE Fleks, a KorGE port of the Fleks library, an ECS (Entity Component System library)

Supports triangulating, shape2D operations & mesh path finding.

Adds support for A* pathfinding for bidimensional boolean arrays: true for blocking cells, false for available cells.

val map = AStar(BooleanArray2.fromString(mapOf('X' to true, '.' to false), false, code = """
    .X...X....
    .X...X....
    .X.X.X....
    ...X.X....
    ...X......
    ...X......
""".trimIndent()))
val points = map.find(0, 1, 6, 2, findClosest = true, diagonals = true)
println(points)
// [(0, 1), (0, 2), (1, 3), (2, 2), (3, 1), (4, 2), (4, 3), (5, 4), (6, 3), (6, 2)]

Supports encoding and decoding JPG/JPEGs in pure Kotlin. Typically not necessary, since KorIM will use native decoders for JPEG and PNG. But useful to run on Node, or to be able to encode images.

val pngBytes = resourcesVfs["korge.png"].readBytes()
val bitmap = resourcesVfs["korge.png"].readBitmap()
val jpegBytes = measureTime({ JPEG.encode(bitmap, ImageEncodingProps(quality = 0.1)) }) { println("ENCODED in $it") }
val image = measureTime({ JPEG.decode(jpegBytes) }) { println("DECODED in $it") }
image(image).alpha(0.25)

Supports generating QR codes:

image(QR.msg("Hello from KorIM-QR!")).xy(128, 128).scale(6.0).also { it.smoothing = false }//.filters(DropshadowFilter(0.0, 0.0, blurRadius = 12.0, shadowColor = Colors.BLACK))

Constructing a QR builder

First we have to construct a QR instance or use the companion object. This class can be constructed with a correctionLevel parameter, and colors for the dark and light areas: colorDark and colorLight.

val qr = QR // Singleton
val qr = QR()
val qr = QR(colorDark = Colors.BLACK, colorLight = Colors.WHITE)
val qr = QR(correctLevel = QRErrorCorrectLevel.H)

Generating a Bitmap32 QR code

With a QR instance already constructed, we can generate a QR code by using the provided methods in the class. QR codes support several kind of contents, and there are methods supporting those contents.

qr.msg(message)
qr.vCard(name, phone, email, url, addr, org, note)
qr.meCard(name, phone, email, url, addr, org)
qr.wifi(ssid, password, WifiKind.WEP)
qr.phone(phone)
qr.email(email)
qr.sms(number, message)
qr.geo(latitude, longitude)
qr.calendarEvent(summary, startDateTime, endDateTime, location, description)

Supports handling keyboard, real gamepad and virtual gamepad all in a simple interface.

val virtualController = virtualController(
    sticks = listOf(
        VirtualStickConfig(
            left = Key.LEFT,
            right = Key.RIGHT,
            up = Key.UP,
            down = Key.DOWN,
            lx = GameButton.LX,
            ly = GameButton.LY,
            anchor = Anchor.BOTTOM_LEFT,
        )
    ),
    buttons = listOf(
        VirtualButtonConfig(
            key = Key.SPACE,
            button = GameButton.BUTTON_SOUTH,
            anchor = Anchor.BOTTOM_RIGHT,
        ),
        VirtualButtonConfig(
            key = Key.RETURN,
            button = GameButton.BUTTON_NORTH,
            anchor = Anchor.BOTTOM_RIGHT,
            offset = Point(0f, -100f)
        )
    ),
)

virtualController.apply {
    down(GameButton.BUTTON_SOUTH) {
        val isInGround = playerSpeed.y.isAlmostZero()
        //if (isInGround) {
        if (true) {
            if (!jumping) {
                jumping = true
                updateState()
            }
            playerSpeed += Vector2(0, -5.5)
        }
    }
    changed(GameButton.LX) {
        if (it.new.absoluteValue < 0.01f) {
            updated(right = it.new > 0f, up = true, scale = 1f)
        }
    }
    addUpdater(60.hz) {
        val lx = virtualController.lx
        when {
            lx < 0f -> {
                updated(right = false, up = false, scale = lx.absoluteValue)
            }
            lx > 0f -> {
                updated(right = true, up = false, scale = lx.absoluteValue)
            }
        }
    }
}

Adds support for https://admob.google.com/.

Supports

KorGE 4.0.0

Usage

In build.gradle.kts:

korge {
    androidPermission("INTERNET")
    androidManifestApplicationChunk(
        "<meta-data android:name=\"com.google.android.gms.ads.APPLICATION_ID\" android:value=\"ca-app-pub-3395905965441916~3606887124\" />"
    )
}

In your fun main() = Korge {:

val admob = AdmobCreate(views, testing = true)
admob.bannerPrepare(
    Admob.Config(
        id = "ca-app-pub-3395905965441916/9312372956"
    )
)
admob.bannerShow()

Some code to get started

fun chunk(pos: PointInt, data: String): StackedIntArray2 = StackedIntArray2(IntArray2(data, gen = { c, x, y -> if (c == '.') 0 else 1 }), startX = pos.x, startY = pos.y)

val fullMap = SparseChunkedStackedIntArray2()
fullMap.putChunk(chunk(PointInt(0, 0), """
    .......
    .......
    .####..
    .......
""".trimIndent()))

val result = fullMap.raycast(RayFromTwoPoints(Point(12, 12), Point(120, 80)), Size(8, 8)) { this.getLast(it.x, it.y) != 0 }
assertEquals(PointInt(18, 16), result?.toInt())

Visual testing scene

import korlibs.datastructure.*
import korlibs.image.atlas.*
import korlibs.image.bitmap.*
import korlibs.image.color.*
import korlibs.image.tiles.*
import korlibs.korge.*
import korlibs.korge.input.*
import korlibs.korge.scene.*
import korlibs.korge.view.*
import korlibs.korge.view.tiles.*
import korlibs.math.geom.*
import korlibs.math.geom.Circle
import korlibs.math.raycasting.*

suspend fun main() = Korge {
    sceneContainer().changeTo({ RaycastingExampleScene()})
}

class RaycastingExampleScene : Scene() {
    @KeepOnReload
    var startPoint: Point = Point(300, 300)

    @KeepOnReload
    var endPoint: Point = Point(100, 100)

    @KeepOnReload
    var tileMap: IntArray2 = IntArray2(100, 100, 0).also {
        for (n in 0 until 10) {
            it[10 + n, 10] = 1
            it[10, 10 + n] = 1
        }
    }

    override suspend fun SContainer.sceneMain() {
        val atlas = MutableAtlasUnit()
        val bmp0 = atlas.add(Bitmap32(16, 16, Colors.TRANSPARENT.premultiplied)).slice
        val bmp1 = atlas.add(Bitmap32(16, 16, Colors.BLUE.premultiplied)).slice
        val tileSet = TileSet(TileSetTileInfo(0, bmp0), TileSetTileInfo(0, bmp1))
        val tileMap = tileMap(tileMap, tileSet)
        val cellSize = Size(bmp0.width, bmp0.height)
        val overlay = graphics(renderer = GraphicsRenderer.SYSTEM) {  }

        text("""
            mouse down: put blocks
            shift+mouse down: remove blocks
            ctrl+mouse down: to change the starting point
        """.trimIndent())

        fun updateOverlay(
            startPoint: Point,
            endPoint: Point,
            result: Point?
        ) {
            overlay.updateShape {
                clear()
                stroke(Colors.YELLOW) { line(startPoint, endPoint) }
                stroke(Colors.GREEN) { circle(Circle(startPoint, 3f)) }
                //stroke(Colors.WHITE) { circle(Circle(endPoint, 3f)) }

                if (result != null) {
                    stroke(Colors.RED) {
                        circle(Circle(result, 3f))
                    }
                }
            }

        }

        fun downOnTileMapPos(mousePos: Point, add: Boolean, setStartPos: Boolean) {
            if (setStartPos) {
                startPoint = mousePos
                return
            }
            val cell = (mousePos / cellSize).toInt()
            tileMap.lock {
                tileMap.stackedIntMap.setFirst(cell.x, cell.y, if (add) 1 else 0)
            }
        }
        fun checkTileMap(mousePos: Point) {
            val result = tileMap.stackedIntMap.raycast(RayFromTwoPoints(startPoint, mousePos), cellSize) {
                //println("this.getLast(it.x, it.y): $it")
                if (!this.inside(it.x, it.y)) return@raycast false
                this.getLast(it.x, it.y) != 0
            }
            updateOverlay(startPoint, mousePos, result)
        }

        tileMap.mouse {
            onDown {
                downOnTileMapPos(it.currentPosLocal, !it.isShiftDown, it.isCtrlDown)
                checkTileMap(it.currentPosLocal)
            }
            onMove {
                if (it.pressing) downOnTileMapPos(it.currentPosLocal, !it.isShiftDown, it.isCtrlDown)
                checkTileMap(it.currentPosLocal)
            }
        }
    }
}

Adds Box2d support to KorGE:

solidRect(920, 100).xy(0, 620).registerBodyWithFixture(type = BodyType.STATIC, friction = 0.2, restitution = 0.2)
for (n in 0 until 5) {
    //fastEllipse(Size(100, 100))
    circle(50f)
    //ellipse(Size(50, 50))
        .xy(120 + 140 * n, 246)
        .anchor(Anchor.CENTER)
        .registerBodyWithFixture(
            type = BodyType.DYNAMIC,
            linearVelocityY = 6.0,
            friction = 0.2,
            restitution = 0.3 + (n * 0.1)
        )
}

Use the LUA scripting engine in KorGE.

val globals = Globals().apply {
    load(BaseLib())
    load(PackageLib())
    load(Bit32Lib())
    load(TableLib())
    load(StringLib())
    load(CoroutineLib())
    LoadState.install(this)
    LuaC.install(this)
}

val textStack = uiVerticalStack(padding = 8f, adjustSize = false).xy(10, 10)

fun luaprintln(str: String) {
    println("LUA_PRINTLN: $str")
    textStack.text(str)
    //kotlin.io.println()
}

// Overwrite print function
globals["print"] = object : VarArgFunction() {
    override fun invoke(args: Varargs): Varargs {
        val tostring = globals["tostring"]
        val out = (1 .. args.narg())
            .map { tostring.call(args.arg(it)).strvalue()!!.tojstring() }
        luaprintln(out.joinToString("\t"))
        return LuaValue.NONE
    }
}
val result = globals.load(
    //language=lua
    """
    function max(a, b)
        if (a > b) then
            return a
        else
            return b
        end
    end
    a = 10
    res = 1 + 2 + a + max(20, 30)
    print(res - 1)
    b = {}
    b[1] = 10
    print(b)
    for i=4,1,-1 do print(i) end
    
    
    co = coroutine.create(function ()
       for i=1,5 do
         --print("co", i)
         coroutine.yield("co" .. i, i + 1)
       end
       return "completed"
     end)
     
    function coroutine_it (co)
      return function ()
            local code, res = coroutine.resume(co)
            if code then
                return res 
            end
     end
    end

    for i in coroutine_it(co) do
        print("for", i)
    end
    
    --for i=1,12 do
    --    local code, res = coroutine.resume(co)
    --    print(code, res)
    --end
    print("ENDED!")

    return res
""").callSuspend()

luaprintln(result.toString())

Examples:

val skeDeferred = async { Buffer(res["mecha_1002_101d_show/mecha_1002_101d_show_ske.dbbin"].readBytes()) }
val texDeferred = async { res["mecha_1002_101d_show/mecha_1002_101d_show_tex.json"].readString() }
val imgDeferred = async { res["mecha_1002_101d_show/mecha_1002_101d_show_tex.png"].readBitmap().mipmaps() }

val data = factory.parseDragonBonesData(skeDeferred.await())
val atlas = factory.parseTextureAtlasData(Json.parseFast(texDeferred.await())!!, imgDeferred.await())

val armatureDisplay = factory.buildArmatureDisplay("mecha_1002_101d")!!.position(0, 300).scale(SCALE)

println(armatureDisplay.animation.animationNames)
armatureDisplay.animation.play("idle")
this += armatureDisplay

Adds support for Spine.

val atlas = resourcesVfs["spineboy/spineboy-pma.atlas"].readAtlas(ImageDecodingProps(asumePremultiplied = true))
val skeletonData = resourcesVfs["spineboy/spineboy-pro.skel"].readSkeletonBinary(atlas, 0.6f)

fun createSkel(): Pair<Skeleton, AnimationState> {
    val skeleton = Skeleton(skeletonData) // Skeleton holds skeleton state (bone positions, slot attachments, etc).
    val stateData = AnimationStateData(skeletonData) // Defines mixing (crossfading) between animations.
    stateData.setMix("run", "jump", 0.2f)
    stateData.setMix("jump", "run", 0.2f)

    val state = AnimationState(stateData) // Holds the animation state for a skeleton (current animation, time, etc).
    state.timeScale = 0.5f // Slow all animations down to 50% speed.

    // Queue animations on track 0.
    state.setAnimation(0, "run", true)
    state.addAnimation(0, "jump", false, 2f) // Jump after 2 seconds.
    state.addAnimation(0, "run", true, 0f) // Run after the jump.
    state.update(1f / 60f) // Update the animation time.
    state.apply(skeleton) // Poses skeleton using current animations. This sets the bones' local SRT.

    skeleton.updateWorldTransform() // Uses the bones' local SRT to compute their world SRT.
    return skeleton to state
}

// Add view
container {
    val (skeleton, state) = createSkel()
    //speed = 2.0f
    speed = 0.5f
    scale(2.0)
    position(200, 700)
    skeletonView(skeleton, state).also { it.debugAnnotate = true }
    solidRect(10.0, 10.0, Colors.RED).centered
    filters(DropshadowFilter(shadowColor = Colors.RED))
}

container {
    val (skeleton, state) = createSkel()
    //speed = 2.0f
    speed = 1.0f
    scale(2.0)
    position(900, 700)
    skeletonView(skeleton, state).also { it.debugAnnotate = true }
    solidRect(10.0, 10.0, Colors.RED).centered
    //filters(DropshadowFilter(shadowColor = Colors.RED))
}

Use Compose Multiplatform along KorGE.

In order to use this module you need to do some manual additions for now:

In your root build.gradle.kts

plugins {
    // ...
	id("org.jetbrains.compose") version "1.4.0" 
}

// ...

compose {
	kotlinCompilerPluginArgs.add("suppressKotlinVersionCompatibilityCheck=1.8.21")
	kotlinCompilerPlugin.set(dependencies.compiler.forKotlin("1.8.20"))
}

And it also requires at least kproject 0.2.7.

Example. Using the Vampires asset:

import androidx.compose.runtime.*
import com.finalbossblues.timefantasy.*
import korlibs.event.*
import korlibs.image.bitmap.*
import korlibs.image.color.*
import korlibs.image.format.*
import korlibs.io.file.std.*
import korlibs.io.stream.*
import korlibs.korge.*
import korlibs.korge.compose.*
import korlibs.korge.scene.*
import korlibs.korge.view.*
import korlibs.math.geom.*
import korlibs.math.geom.Anchor
import korlibs.korge.compose.Image
import korlibs.korge.ui.*
import korlibs.korge.view.animation.*

suspend fun main() = Korge(
    title = "Korge Compose",
    windowSize = Size(512, 512),
    backgroundColor = Colors["#2b2b2b"],
    displayMode = KorgeDisplayMode(ScaleMode.SHOW_ALL, Anchor.TOP_LEFT, clipBorders = false),
    forceRenderEveryFrame = false
) {
    val sceneContainer = sceneContainer()

    sceneContainer.changeTo({ MyScene() })
}

class MyScene : Scene() {
    override suspend fun SContainer.sceneMain() {
        setComposeContent(this) {
            var width by remember { mutableStateOf(width.toInt()) }
            var height by remember { mutableStateOf(height.toInt()) }
            LaunchedEffect(true) {
                fun onResized() {
                    val w = views.actualVirtualWidth
                    val h = views.actualVirtualHeight
                    width = w
                    height = h
                    this@sceneMain.size(w.toDouble(), h.toDouble())
                }

                onEvent(ReshapeEvent) {
                    onResized()
                }
                onResized()
                //onStageResized { w, h ->
                //    //println("RESIZED: $w, $h")
                //    this@sceneMain.size(w.toDouble(), h.toDouble())
                //}
            }
            MainApp(width, height)
        }
    }
}

@Composable
fun ImageDataView(
    data: ImageData? = null,
    animation: String? = null,
) {
    ComposeKorgeView({
        UIContainer(Size(32, 32)).apply {
            imageDataView(data, animation)
                .xy(16, 16)
                .also { it.play() }
        }
    }, {
        set(data) { (this.firstChild as korlibs.korge.view.animation.ImageDataView).data = data }
        set(animation) { (this.firstChild as korlibs.korge.view.animation.ImageDataView).animation = animation }
    })
}

@Composable
private fun MainApp(width: Int, height: Int) {
    var color by remember { mutableStateOf(Colors.WHITE) }
    var count by remember { mutableStateOf(0) }
    var bitmap: BmpSlice by remember { mutableStateOf(Bitmaps.transparent) }
    var vampire: ImageData? by remember { mutableStateOf(null) }

    fun insert(digit: Int) {
        count *= 10
        count += digit
    }

    LaunchedEffect(true) {
        val vampires = EvilTransformingVampires.readImages()
        vampire = vampires?.vampire
        bitmap = vampires?.vampire?.defaultAnimation?.firstFrame?.bitmap?.slice() ?: Bitmaps.white
        println("1")
        //bitmap = resourcesVfs["korge-30.jpg"].readBitmap().slice()
    }

    VStack(width.toFloat(), adjustSize = true) {
        Image(bitmap)
        ImageDataView(vampire, "down")
        Text("$count", color)
        HStack {
            Button("-") { count-- }
            Button("+") { count++ }
        }
    }
}