From 0e6e9c4791e54b135f55104ea5ada0e8bdff8dd0 Mon Sep 17 00:00:00 2001 From: jobeGameDev Date: Tue, 22 Oct 2024 07:11:41 +0200 Subject: [PATCH] Implement loading of entity configs from entity layer in ldtk (#30) * 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 --- korge-fleks/kproject.yml | 2 +- .../korlibs/korge/fleks/assets/AssetModel.kt | 1 - .../korlibs/korge/fleks/assets/AssetStore.kt | 117 ++++++++++++++---- .../fleks/components/LdtkLevelMapComponent.kt | 18 ++- .../fleks/entity/config/LevelMapConfig.kt | 12 +- .../korge/fleks/gameState/GameStateManager.kt | 69 ++--------- .../renderSystems/LDtkLevelMapRenderSystem.kt | 6 +- 7 files changed, 119 insertions(+), 106 deletions(-) diff --git a/korge-fleks/kproject.yml b/korge-fleks/kproject.yml index 55cbb5c..453cf6b 100644 --- a/korge-fleks/kproject.yml +++ b/korge-fleks/kproject.yml @@ -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 # diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetModel.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetModel.kt index 937c1e7..13fe4f7 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetModel.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetModel.kt @@ -33,4 +33,3 @@ data class AssetModel( } enum class AssetType { COMMON, WORLD, LEVEL, SPECIAL } -enum class TileMapType { LDTK, TILED } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetStore.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetStore.kt index d6b5bb5..f375629 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetStore.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetStore.kt @@ -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 +typealias LayerEntityMaps = MutableMap> /** * This class is responsible to load all kind of game data and make it usable / consumable by entities of Korge-Fleks. @@ -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> = mutableMapOf() - internal val levelLayerTileMaps: MutableMap> = mutableMapOf() + internal val levelMaps: MutableMap> = mutableMapOf() // 1st string: Level name, 2nd string: Layer name + internal val entityConfigMaps: MutableMap> = mutableMapOf() // 1st string: Level name, 2nd string: Layer name internal var backgrounds: MutableMap> = mutableMapOf() internal var images: MutableMap> = mutableMapOf() internal var fonts: MutableMap> = mutableMapOf() internal var sounds: MutableMap> = 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!") @@ -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 = + 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)) { @@ -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() + + 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 } } } @@ -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? = @@ -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 } } } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/components/LdtkLevelMapComponent.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/components/LdtkLevelMapComponent.kt index 323b0ce..30f5eff 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/components/LdtkLevelMapComponent.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/components/LdtkLevelMapComponent.kt @@ -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? = null, + var layerNames: List = 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, @@ -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() diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/LevelMapConfig.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/LevelMapConfig.kt index 7a2e7b1..7b9a3c5 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/LevelMapConfig.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/LevelMapConfig.kt @@ -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? = null, // Optional: Show only specific layers - private val levelLayer: String = "", // The level and layer names "_" in the LDtk world + private val levelName: String, // Unique name for level within a world + private val layerNames: List, // List of names for layers to show private val layerTag: RenderLayerTag, private val x: Float = 0f, private val y: Float = 0f, @@ -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 diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/gameState/GameStateManager.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/gameState/GameStateManager.kt index c16d0dd..a7757f3 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/gameState/GameStateManager.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/gameState/GameStateManager.kt @@ -18,13 +18,12 @@ object GameStateManager { var firstGameStart: Boolean = true internal lateinit var gameStateConfig: GameStateConfig - private val gameStateSerializer = EntityConfigSerializer() /** * Register a new serializers module for the entity config serializer. */ fun register(name: String, module: SerializersModule) { - gameStateSerializer.register(name, module) + assetStore.configDeserializer.register(name, module) } /** @@ -39,7 +38,7 @@ object GameStateManager { val gameStateConfigString = gameStateConfigVfs.readString() try { - gameStateConfig = gameStateSerializer.yaml().decodeFromString(gameStateConfigString) + gameStateConfig = assetStore.configDeserializer.yaml().decodeFromString(gameStateConfigString) } catch (e: Throwable) { gameStateConfig = GameStateConfig("", true, "", "", "") println("ERROR: Loading game state config - $e") @@ -56,61 +55,13 @@ object GameStateManager { val gameConfigString = vfs.readString() try { - val commonConfig = gameStateSerializer.yaml().decodeFromString(gameConfigString) + val commonConfig = assetStore.configDeserializer.yaml().decodeFromString(gameConfigString) // Enable / disable hot reloading of common assets here assetStore.loadAssets(AssetType.COMMON, assetConfig = commonConfig) } catch (e: Throwable) { println("ERROR: Loading common assets - $e") } // } else println("WARNING: Cannot find common entity config file 'common/config.yaml'!") } - /** - * Load all entity configs from LDtk level and put them into the EntityFactory. - */ - private fun loadEntityConfigsFromLdtkLevel(worldName: String, levelName: String) { - var gameObjectCnt = 0 - - val gridSize = assetStore.getLdtkWorld(worldName).ldtk.defaultGridSize - val entityLayer = assetStore.getLdtkLevel(assetStore.getLdtkWorld(worldName), levelName).layerInstances?.firstOrNull { it.entityInstances.isNotEmpty() } - entityLayer?.entityInstances?.forEach { entity -> - println("INFO: Game object '${entity.identifier}' loaded for '$levelName'") - - // 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) { - - // Add scripts with their original name - if (entity.tags.firstOrNull { it == "script" } != null) yamlString.append("- name: ${entity.identifier}\n") - // Add other game objects with an unique name as identifier - else yamlString.append("- name: ${worldName}_${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(yamlString) - - } else println("ERROR: Game object with name '${entity.identifier}' has no field entityConfig") - - try { - val entityConfigs: List = - gameStateSerializer.yaml().decodeFromString(yamlString.toString()) - entityConfigs.forEach { entityConfig -> - EntityFactory.register(entityConfig) - } - println("INFO: Entity config '${entity.identifier}' loaded for '$levelName'") - } catch (e: Throwable) { - println("ERROR: Loading entity config - $e") - } - } - } - /** * Function for loading word, level and special game config and assets. This function should be called * whenever new assets need to be loaded. I.e. also within a level when assets of type 'special' should @@ -130,7 +81,7 @@ object GameStateManager { var gameConfigString = worldVfs.readString() try { - val worldConfig = gameStateSerializer.yaml().decodeFromString(gameConfigString) + val worldConfig = assetStore.configDeserializer.yaml().decodeFromString(gameConfigString) // Enable / disable hot reloading of common assets here assetStore.loadAssets(AssetType.WORLD, assetConfig = worldConfig) } catch (e: Throwable) { @@ -143,7 +94,7 @@ object GameStateManager { // if (levelVfs.exists()) { gameConfigString = levelVfs.readString() try { - val worldConfig = gameStateSerializer.yaml().decodeFromString(gameConfigString) + val worldConfig = assetStore.configDeserializer.yaml().decodeFromString(gameConfigString) // Enable / disable hot reloading of common assets here assetStore.loadAssets(AssetType.LEVEL, assetConfig = worldConfig) } catch (e: Throwable) { @@ -157,7 +108,7 @@ object GameStateManager { // if (vfs.exists()) { gameConfigString = vfs.readString() try { - val specialConfig = gameStateSerializer.yaml().decodeFromString(gameConfigString) + val specialConfig = assetStore.configDeserializer.yaml().decodeFromString(gameConfigString) // Enable / disable hot reloading of common assets here assetStore.loadAssets(AssetType.SPECIAL, assetConfig = specialConfig) } catch (e: Throwable) { @@ -165,9 +116,6 @@ object GameStateManager { } // } else println("WARNING: Cannot find special game config file '${gameStateConfig.special}/${gameStateConfig.special}/config.yaml'!") } - - loadEntityConfigsFromLdtkLevel(gameStateConfig.world, gameStateConfig.special) // here entity configs for "intro" are loaded - loadEntityConfigsFromLdtkLevel(gameStateConfig.world, gameStateConfig.level) } /** @@ -178,7 +126,10 @@ object GameStateManager { * EntityConfig type. The type can be set in LDtk level map editor. */ fun startGame(world: World) { - val startScript = "start_script" + // TODO: Check if save game is available and load it + + // Load start script from level + val startScript = "${gameStateConfig.level}_start_script" if (EntityFactory.contains(startScript)) { println("INFO: Starting '${gameStateConfig.level}' with script: '$startScript'.") world.createAndConfigureEntity(startScript) diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LDtkLevelMapRenderSystem.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LDtkLevelMapRenderSystem.kt index fbdc382..9e65bdf 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LDtkLevelMapRenderSystem.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LDtkLevelMapRenderSystem.kt @@ -34,7 +34,6 @@ class LDtkLevelMapRenderSystem( // Debugging layer rendering private var renderLayer = 0 - @OptIn(KorgeExperimental::class) override fun renderInternal(ctx: RenderContext) { // Sort level maps by their layerIndex family.sort(comparator) @@ -42,12 +41,11 @@ class LDtkLevelMapRenderSystem( // Iterate over all entities which should be rendered in this view family.forEach { entity -> val (x, y, offsetX, offsetY) = entity[PositionComponent] - val ldtkLevelMapComponent = entity[LdtkLevelMapComponent] - val levelLayer = ldtkLevelMapComponent.levelLayer + val (levelName, layerNames) = entity[LdtkLevelMapComponent] val rgba = Colors.WHITE // TODO: use here alpha from ldtk layer - val tileMap = assetStore.getTileMapData(levelLayer) + val tileMap = assetStore.getTileMapData(levelName, layerNames.first()) // TODO: Render all layers, not only first one val tileSet = tileMap.tileSet val tileSetWidth = tileSet.width val tileSetHeight = tileSet.height