Skip to content

Commit

Permalink
Implement loading of entity configs from entity layer in ldtk (#30)
Browse files Browse the repository at this point in the history
* Load entity configs from entity layers of levels in LDtk
* Update korge-ldtk to latest commit
* Cleanup game state manager and start refactoring level data in asset store
  • Loading branch information
jobe-m authored Oct 22, 2024
1 parent 4ab3c99 commit 0e6e9c4
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 106 deletions.
2 changes: 1 addition & 1 deletion korge-fleks/kproject.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dependencies:
#
- maven::common::com.charleskorn.kaml:kaml:0.61.0 # 0.59.0 <-- last version supporting kotlin 1.9.x and serialization 1.6.x
- maven::common::com.soywiz.korlibs.korge2:korge
- https://github.com/korlibs/korge-ldtk/tree/v1.0.3/korge-ldtk
- https://github.com/korlibs/korge-ldtk/tree/bc5cbb726f78aa3064357d9babba67fbc5ad7db1/korge-ldtk
# Use local copy of KorGE addons
# - ../../korge-ldtk/korge-ldtk
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,3 @@ data class AssetModel(
}

enum class AssetType { COMMON, WORLD, LEVEL, SPECIAL }
enum class TileMapType { LDTK, TILED }
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import korlibs.image.font.readBitmapFont
import korlibs.image.format.*
import korlibs.image.tiles.*
import korlibs.io.file.std.resourcesVfs
import korlibs.korge.fleks.entity.*
import korlibs.korge.fleks.gameState.GameStateManager.assetStore
import korlibs.korge.fleks.utils.*
import korlibs.korge.ldtk.*
import korlibs.korge.ldtk.view.*
import korlibs.memory.*
import korlibs.time.Stopwatch
import korlibs.math.max
import kotlinx.serialization.*
import kotlin.collections.set
import kotlin.math.max

typealias LayerTileMaps = MutableMap<String, TileMapData>
typealias LayerEntityMaps = MutableMap<String, List<EntityConfig>>

/**
* This class is responsible to load all kind of game data and make it usable / consumable by entities of Korge-Fleks.
Expand All @@ -36,19 +42,24 @@ class AssetStore {
val levelAtlas: MutableAtlasUnit = MutableAtlasUnit(1024, 2048, border = 1)
val specialAtlas: MutableAtlasUnit = MutableAtlasUnit(1024, 2048, border = 1)

// @Volatile
val configDeserializer = EntityConfigSerializer()

// @Volatile
internal var commonAssetConfig: AssetModel = AssetModel()
internal var currentWorldAssetConfig: AssetModel = AssetModel()
internal var currentLevelAssetConfig: AssetModel = AssetModel()
internal var specialAssetConfig: AssetModel = AssetModel()

internal var ldtkWorlds: MutableMap<String, Pair<AssetType, LDTKWorld>> = mutableMapOf()
internal val levelLayerTileMaps: MutableMap<String, Pair<AssetType, TileMapData>> = mutableMapOf()
internal val levelMaps: MutableMap<String, Pair<AssetType, LayerTileMaps>> = mutableMapOf() // 1st string: Level name, 2nd string: Layer name
internal val entityConfigMaps: MutableMap<String, Pair<AssetType, LayerEntityMaps>> = mutableMapOf() // 1st string: Level name, 2nd string: Layer name
internal var backgrounds: MutableMap<String, Pair<AssetType, ParallaxDataContainer>> = mutableMapOf()
internal var images: MutableMap<String, Pair<AssetType, ImageDataContainer>> = mutableMapOf()
internal var fonts: MutableMap<String, Pair<AssetType, Font>> = mutableMapOf()
internal var sounds: MutableMap<String, Pair<AssetType, SoundChannel>> = mutableMapOf()

// TODO: Create data class for storing level data
// grizSize, entities, tileMapData

fun getSound(name: String) : SoundChannel =
if (sounds.contains(name)) sounds[name]!!.second
else error("AssetStore: Sound '$name' not found!")
Expand All @@ -70,17 +81,19 @@ class AssetStore {
ImageData()
}

fun getLdtkWorld(name: String) : LDTKWorld =
if (ldtkWorlds.contains(name)) ldtkWorlds[name]!!.second
else error("AssetStore: LDtkWorld '$name' not found!")

fun getLdtkLevel(ldtkWorld: LDTKWorld, levelName: String) : Level =
if (ldtkWorld.levelsByName.contains(levelName)) ldtkWorld.levelsByName[levelName]!!.level
else error("AssetStore: LDtkLevel '$levelName' not found!")
fun getTileMapData(level: String, layer: String) : TileMapData =
if (levelMaps.contains(level)) {
if (levelMaps[level]!!.second.contains(layer)) levelMaps[level]!!.second[layer]!!
else error("AssetStore: TileMap layer '$layer' for level '$level' not found!")
}
else error("AssetStore: Level map for level '$level' not found!")

fun getTileMapData(levelLayer: String) : TileMapData =
if (levelLayerTileMaps.contains(levelLayer)) levelLayerTileMaps[levelLayer]!!.second
else error("AssetStore: TileMap for level-layer '$levelLayer' not found!")
fun getEntityConfigs(level: String, layer: String) : List<EntityConfig> =
if (entityConfigMaps.contains(level)) {
if (entityConfigMaps[level]!!.second.contains(layer)) entityConfigMaps[level]!!.second[layer]!!
else error("AssetStore: Entity layer '$layer' for level '$level' not found!")
}
else error("AssetStore: EntityConfig for level '$level' not found!")

fun getNinePatch(name: String) : NinePatchBmpSlice =
if (images.contains(name)) {
Expand Down Expand Up @@ -138,20 +151,78 @@ class AssetStore {
val sw = Stopwatch().start()
println("AssetStore: Start loading [${type.name}] resources from '${assetConfig.folder}'...")

var gameObjectCnt = 0

// Update maps of music, images, ...
assetConfig.tileMaps.forEach { tileMap ->
val ldtkWorld = resourcesVfs[assetConfig.folder + "/" + tileMap.fileName].readLDTKWorld(extrude = true)

// TODO: Hardcoded - will be removed later anyway - needed still for loading entity instances (start script of intro)
ldtkWorlds["world_1"] = Pair(type, ldtkWorld)

// Save TileMapData for each Level and layer combination from LDtk world
ldtkWorld.ldtk.levels.forEach { ldtkLevel ->
val levelName = ldtkLevel.identifier
ldtkLevel.layerInstances?.forEach { ldtkLayer ->
val layerName = ldtkLayer.identifier
val gridSize = ldtkLayer.gridSize
// Check if layer has tile set -> store tile map data
val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid]

if (tilesetExt != null) {
storeTiles(ldtkLayer, tilesetExt, ldtkLevel.identifier, ldtkLayer.identifier, type)
storeTiles(ldtkLayer, tilesetExt, levelName, layerName, type)
}
// Check if layer contains entity data -> create EntityConfigs and store them fo
if (ldtkLayer.entityInstances.isNotEmpty()) {
val entityList = mutableListOf<EntityConfig>()

ldtkLayer.entityInstances.forEach { entity ->
// Create YAML string of an entity config from LDtk
val yamlString = StringBuilder()
// Sanity check - entity needs to have a field 'entityConfig'
if (entity.fieldInstances.firstOrNull { it.identifier == "entityConfig" } != null) {

if (entity.tags.firstOrNull { it == "script" } != null) {
// Add scripts without unique count value - they are unique by name because they exist only once
yamlString.append("name: ${levelName}_${entity.identifier}\n")
}
else {
// Add other game objects with a unique name as identifier
yamlString.append("name: ${levelName}_${entity.identifier}_${gameObjectCnt++}\n")
}

// Add position of entity
entity.tags.firstOrNull { it == "positionable" }?.let {
yamlString.append("x: ${entity.gridPos.x * gridSize}\n")
yamlString.append("y: ${entity.gridPos.y * gridSize}\n")
}

// Add all other fields of entity
entity.fieldInstances.forEach { field ->
if (field.identifier != "EntityConfig") yamlString.append("${field.identifier}: ${field.value}\n")
}
println("INFO: Game object '${entity.identifier}' loaded for '$levelName'")
println("\n$yamlString")

try {
// By deserializing the YAML string we get an EntityConfig object which itself registers in the EntityFactory
val entityConfig: EntityConfig = configDeserializer.yaml().decodeFromString(yamlString.toString())

// TODO: We need to store only the name of the entity config for later dynamically spawning of entities
// We need to store the entity configs in a 2D array depending on its position in the level
// Then later we will spawn the entities depending on the position in the level
entityList.add(entityConfig)

println("INFO: Registering entity config '${entity.identifier}' for '$levelName'")
} catch (e: Throwable) {
println("ERROR: Loading entity config - $e")
}

} else println("ERROR: Game object with name '${entity.identifier}' has no field entityConfig!")
}

// Create new map for Entity layer if it does not exist yet
if (!entityConfigMaps.contains(levelName)) {
val layerEntityMaps: LayerEntityMaps = mutableMapOf()
entityConfigMaps[levelName] = Pair(type, layerEntityMaps)
}
// Finally store entity config for level and entity layer
entityConfigMaps[levelName]!!.second[layerName] = entityList
}
}
}
Expand Down Expand Up @@ -243,7 +314,11 @@ class AssetStore {
}
}
}
levelLayerTileMaps["${level}_${layer}"] = Pair(type, tileMapData)
// Create new map for level layers and store layer in it
val layerTileMaps: LayerTileMaps = mutableMapOf()
layerTileMaps[layer] = tileMapData
// Add layer map to level Maps
levelMaps[level] = Pair(type, layerTileMaps)
}

private fun prepareCurrentAssets(type: AssetType, newAssetConfig: AssetModel, currentAssetConfig: AssetModel): AssetModel? =
Expand Down Expand Up @@ -276,6 +351,6 @@ class AssetStore {
images.values.removeAll { it.first == type }
fonts.values.removeAll { it.first == type }
sounds.values.removeAll { it.first == type }
levelLayerTileMaps.values.removeAll { it.first == type }
levelMaps.values.removeAll { it.first == type }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,20 @@ import kotlinx.serialization.*

@Serializable @SerialName("LdtkLevelMap")
data class LdtkLevelMapComponent(
/**
* The name of the LDtk world in the Asset manager
*/
var worldName: String = "",
/**
* The unique identifier (level name) of the level from the LDtk world
*/
var levelName: String = "",
/**
* Optional: List of layer names which shall be drawn by the specific render system.
* If not set, all layers will be drawn.
* List of layer names which shall be drawn by the specific render system.
*
* Example: ["Background", "Playfield"]
*/
var layerNames: List<String>? = null,
var layerNames: List<String> = listOf(),

var levelLayer: String = "", // The level and layer name in the LDtk world

// TODO: Move this to AssetStore
var width: Float = 0f, // Size of the level map
var height: Float = 0f,

Expand All @@ -41,10 +37,10 @@ data class LdtkLevelMapComponent(
else initialized = true

val assetStore: AssetStore = this.inject(name = "AssetStore")

val ldtkLevel = assetStore.getLdtkLevel(assetStore.getLdtkWorld(worldName), levelName)
width = ldtkLevel.pxWid.toFloat()
height = ldtkLevel.pxHei.toFloat()
val tileMapData = assetStore.getTileMapData(levelName, layerNames.first())
// TODO: remove hardcoded values - assetStore.getLevelWidth(levelName) : Float
width = (tileMapData.data.width * 16).toFloat()
height = (tileMapData.data.height * 16).toFloat()
}

companion object : ComponentType<LdtkLevelMapComponent>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@ import kotlinx.serialization.*
data class LevelMapConfig(
override val name: String,

private val mapType: TileMapType,
private val assetName: String = "", // Used with LDtk and Tiled based maps
private val levelName: String = "", // Used with LDtk based maps
private val layerNames: List<String>? = null, // Optional: Show only specific layers
private val levelLayer: String = "", // The level and layer names "<level>_<layer>" in the LDtk world
private val levelName: String, // Unique name for level within a world
private val layerNames: List<String>, // List of names for layers to show
private val layerTag: RenderLayerTag,
private val x: Float = 0f,
private val y: Float = 0f,
Expand All @@ -34,10 +31,7 @@ data class LevelMapConfig(

override fun World.entityConfigure(entity: Entity) : Entity {
entity.configure {
when (mapType) {
TileMapType.LDTK -> it += LdtkLevelMapComponent(assetName, levelName, layerNames, levelLayer)
TileMapType.TILED -> it += TiledLevelMapComponent(assetName)
}
it += LdtkLevelMapComponent(levelName, layerNames)
it += PositionComponent(
x = x,
y = y
Expand Down
Loading

0 comments on commit 0e6e9c4

Please sign in to comment.