diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetLevelData.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetLevelData.kt index f703dca..9d27d60 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetLevelData.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetLevelData.kt @@ -1,5 +1,6 @@ package korlibs.korge.fleks.assets +import korlibs.image.bitmap.* import korlibs.image.tiles.* import korlibs.korge.fleks.utils.* import korlibs.korge.ldtk.* @@ -12,78 +13,150 @@ import kotlin.math.* class AssetLevelData { internal val worldData = WorldData() - // TODO: move into WorldData - internal val levelDataMaps: MutableMap = mutableMapOf() internal val configDeserializer = EntityConfigSerializer() private var gameObjectCnt = 0 + // Size of a level within the grid vania array in grid coordinates (index) private var gridVaniaWidth: Int = 0 private var gridVaniaHeight: Int = 0 /** - * @param hasParallax - This level uses a parallax background, so we need to set the world size accordingly. - * TODO check if we need this also later - do we have only one active world level? + * Load level data from LDtk world file and store it in the worldData object. + * The worldData object contains all level data and is used to render the levels. + * The level data is stored in a 2D array where each element is a LevelData object. + * The LevelData object contains the tile map data and entity data for each level. + * + * @param ldtkWorld - LDtk world object containing all level data + * + * @see WorldData */ - fun loadLevelData(ldtkWorld: LDTKWorld, type: AssetType, hasParallax: Boolean) { + fun loadLevelData(ldtkWorld: LDTKWorld) { gridVaniaWidth = ldtkWorld.ldtk.worldGridWidth ?: 1 // this is also the size of each sub-level gridVaniaHeight = ldtkWorld.ldtk.worldGridHeight ?: 1 - - // Get the highest values for X and Y axis - this will be the size of the grid vania array var maxLevelOffsetX = 0 var maxLevelOffsetY = 0 + + // Get the highest values for X and Y axis - this will be the size of the grid vania array ldtkWorld.ldtk.levels.forEach { ldtkLevel -> if (maxLevelOffsetX < ldtkLevel.worldX) maxLevelOffsetX = ldtkLevel.worldX if (maxLevelOffsetY < ldtkLevel.worldY) maxLevelOffsetY = ldtkLevel.worldY } // Create grid vania array - val sizeX: Int = (maxLevelOffsetX / gridVaniaWidth) + 1 - val sizeY: Int = (maxLevelOffsetY / gridVaniaHeight) + 1 - worldData.levelGridVania = List(sizeX) { List(sizeY) { LevelData() } } + worldData.gridWidth = (maxLevelOffsetX / gridVaniaWidth) + 1 + worldData.gridHeight = (maxLevelOffsetY / gridVaniaHeight) + 1 // Set the size of the world - worldData.width = (sizeX * gridVaniaWidth).toFloat() - worldData.height = (sizeY * gridVaniaHeight).toFloat() + worldData.width = (worldData.gridWidth * gridVaniaWidth).toFloat() + worldData.height = (worldData.gridHeight * gridVaniaHeight).toFloat() if (maxLevelOffsetX == 0) println("WARNING: Level width is 0!") // Save TileMapData for each Level and layer combination from LDtk world ldtkWorld.ldtk.levels.forEach { ldtkLevel -> - val globalLevelPosX = ldtkLevel.worldX - val globalLevelPosY = ldtkLevel.worldY - val levelX: Int = globalLevelPosX / gridVaniaWidth - val levelY: Int = globalLevelPosY / gridVaniaHeight - - loadLevel(worldData.levelGridVania[levelX][levelY], ldtkWorld, ldtkLevel, type) + val levelX: Int = ldtkLevel.worldX / gridVaniaWidth + val levelY: Int = ldtkLevel.worldY / gridVaniaHeight + loadLevel(worldData, levelX, levelY, ldtkWorld, ldtkLevel) } - println("Gridvania size: $sizeX x $sizeY)") + println("Gridvania size: ${worldData.gridWidth} x ${worldData.gridHeight})") } - fun reloadAsset(ldtkWorld: LDTKWorld, type: AssetType) { + fun reloadAsset(ldtkWorld: LDTKWorld) { // Reload all levels from ldtk world file ldtkWorld.ldtk.levels.forEach { ldtkLevel -> - ldtkLevel.layerInstances?.forEach { ldtkLayer -> - val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] + val levelX: Int = ldtkLevel.worldX / gridVaniaWidth + val levelY: Int = ldtkLevel.worldY / gridVaniaHeight + loadLevel(worldData, levelX, levelY, ldtkWorld, ldtkLevel) + } + } + + private fun loadLevel(worldData: WorldData, levelX: Int, levelY: Int, ldtkWorld: LDTKWorld, ldtkLevel: Level) { + val levelName = ldtkLevel.identifier + + ldtkLevel.layerInstances?.forEach { ldtkLayer -> + val layerName = ldtkLayer.identifier + val gridSize = ldtkLayer.gridSize + + // TODO - we do not need to create a level map for an entity layer!!! + + if (!worldData.layerlevelMaps.contains(layerName)) { + worldData.layerlevelMaps[layerName] = LevelMap( + gridWidth = gridVaniaWidth / gridSize, + gridHeight = gridVaniaHeight / gridSize, + levelGridVania = List(worldData.gridWidth) { List(worldData.gridHeight) { LevelData() } } + ) + } + + // Get level data from worldData + val levelData = worldData.layerlevelMaps[layerName]!!.levelGridVania[levelX][levelY] + + // Check if layer contains entity data -> create EntityConfigs and store them fo + if (ldtkLayer.entityInstances.isNotEmpty()) { + 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 == "unique" } != null) { + // Add scripts without unique count value - they are unique by name because they exist only once + yamlString.append("name: ${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 = (level position in the world) + (grid position within the level) + (pivot point) + // TODO: Take level position in world into account + val entityPosX: Int = /*levelPosX +*/ + (entity.gridPos.x * gridSize) + (entity.pivot[0] * gridSize).toInt() + val entityPosY: Int = /*levelPosY +*/ + (entity.gridPos.y * gridSize) + (entity.pivot[1] * gridSize).toInt() + + + // Add position of entity + entity.tags.firstOrNull { it == "positionable" }?.let { + yamlString.append("x: $entityPosX\n") + yamlString.append("y: $entityPosY\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") - if (tilesetExt != null) { - // Get index of level in the worldData Grid vania array - val levelX: Int = ldtkLevel.worldX / gridVaniaWidth - val levelY: Int = ldtkLevel.worldY / gridVaniaHeight + 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()) - storeTiles(worldData.levelGridVania[levelX][levelY], ldtkLayer, tilesetExt, ldtkLevel.identifier, ldtkLayer.identifier, type) - println("\nTriggering asset change for LDtk level : ${ldtkLevel.identifier}_${ldtkLayer.identifier}") + // 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 + levelData.entities.add(entityConfig.name) + + 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!") } } + + // Check if layer has tile set -> store tile map data + val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] + if (tilesetExt != null) { + storeTiles(levelData, ldtkLayer, tilesetExt) + } } - } - fun removeAssets(type: AssetType) { - levelDataMaps.values.removeAll { it.type == type } } - // TODO: remove "level" - private fun storeTiles(levelData: LevelData, ldtkLayer: LayerInstance, tilesetExt: ExtTileset, level: String, layer: String, type: AssetType) { + private fun storeTiles(levelData: LevelData, ldtkLayer: LayerInstance, tilesetExt: ExtTileset) { val tileMapData = TileMapData( width = ldtkLayer.cWid, height = ldtkLayer.cHei, @@ -187,132 +260,6 @@ class AssetLevelData { } } } - levelData.layerTileMaps[layer] = tileMapData - - // TODO: remove below lines - // Create new map for level layers and store layer in it - val layerTileMaps = mutableMapOf() - layerTileMaps[layer] = tileMapData - // Add layer map to level Maps - if (!levelDataMaps.contains(level)) { - val levelData = LevelData( - type = type, - gridSize = gridSize, - width = (ldtkLayer.cWid * gridSize).toFloat(), - height = (ldtkLayer.cHei * gridSize).toFloat(), - layerTileMaps = layerTileMaps - ) - levelDataMaps[level] = levelData - } else { - levelDataMaps[level]!!.layerTileMaps[layer] = tileMapData - } + levelData.tileMapData = tileMapData } - - private fun loadLevel(levelData: LevelData, ldtkWorld: LDTKWorld, ldtkLevel: Level, type: AssetType) { - val levelName = ldtkLevel.identifier - - ldtkLevel.layerInstances?.forEach { ldtkLayer -> - val layerName = ldtkLayer.identifier - val gridSize = ldtkLayer.gridSize - - // Check if layer contains entity data -> create EntityConfigs and store them fo - if (ldtkLayer.entityInstances.isNotEmpty()) { - 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 == "unique" } != 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 = (level position in the world) + (grid position within the level) + (pivot point) - // TODO: Take level position in world into account - val entityPosX: Int = /*levelPosX +*/ (entity.gridPos.x * gridSize) + (entity.pivot[0] * gridSize).toInt() - val entityPosY: Int = /*levelPosY +*/ (entity.gridPos.y * gridSize) + (entity.pivot[1] * gridSize).toInt() - - - // Add position of entity - entity.tags.firstOrNull { it == "positionable" }?.let { - yamlString.append("x: $entityPosX\n") - yamlString.append("y: $entityPosY\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 - levelData.entities.add(entityConfig.name) - - 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!") - } - - // - levelData.width = (ldtkLayer.cWid * gridSize).toFloat() - levelData.height = (ldtkLayer.cHei * gridSize).toFloat() - - // Create new level data if it does not exist yet - if (!levelDataMaps.contains(levelName)) { - levelDataMaps[levelName] = levelData - } else { - levelDataMaps[levelName]!!.entities - } - } - - // Check if layer has tile set -> store tile map data - val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] - if (tilesetExt != null) { - storeTiles(levelData, ldtkLayer, tilesetExt, levelName, layerName, type) - } - } - } - - /** - * - * @param tileSize - Size of a grid cell in pixels (e.g. 16 for 16x16 tile size) - * @param width - Width of whole level in pixels - * @param height - Height of whole level in pixels - */ - data class WorldData( - var tileSize: Int = 16, - - var width: Float = 0f, - var height: Float = 0f, - var levelWidth: Int = 0, // TODO: Check if we need it outside of this class - level renderer - var levelHeight: Int = 0, - - var levelGridVania: List> = listOf() - ) - - // Data class for storing level data like grizSize, width, height, entities, tileMapData - data class LevelData( - var type: AssetType = AssetType.COMMON, // TODO: Remove - val gridSize: Int = 16, // TODO: Remove - var width: Float = 0f, - var height: Float = 0f, - - val entities: MutableList = mutableListOf(), - val layerTileMaps: MutableMap = mutableMapOf() - ) } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetReload.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetReload.kt index 78dcf6a..653ed78 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetReload.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetReload.kt @@ -189,7 +189,8 @@ class ResourceDirWatcherConfiguration( delay(500) val ldtkWorld = resourcesVfs[assetConfig.folder + "/" + config.fileName].readLDTKWorld(extrude = true) - assetStore.assetLevelData.reloadAsset(ldtkWorld, assetUpdater.type) + println("\nTriggering asset change for LDtk: ${config.fileName}") + assetStore.assetLevelData.reloadAsset(ldtkWorld) // Guard period until reloading is activated again - this is used for debouncing watch messages delay(100) 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 7e385fd..b5da85f 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 @@ -65,36 +65,8 @@ class AssetStore { ImageData() } - fun getTileMapData(level: String, layer: String) : TileMapData = - if (assetLevelData.levelDataMaps.contains(level)) { - if (assetLevelData.levelDataMaps[level]!!.layerTileMaps.contains(layer)) assetLevelData.levelDataMaps[level]!!.layerTileMaps[layer]!! - else error("AssetStore: TileMap layer '$layer' for level '$level' not found!") - } - else error("AssetStore: Level map for level '$level' not found!") - - fun getEntities(level: String) : List = - if (assetLevelData.levelDataMaps.contains(level)) { - assetLevelData.levelDataMaps[level]!!.entities - } - else error("AssetStore: Entities for level '$level' not found!") - - fun getLevelHeight(level: String) : Float = - if (assetLevelData.levelDataMaps.contains(level)) { - assetLevelData.levelDataMaps[level]!!.height - } - else error("AssetStore: Height for level '$level' not found!") - - fun getLevelWidth(level: String) : Float = - if (assetLevelData.levelDataMaps.contains(level)) { - assetLevelData.levelDataMaps[level]!!.width - } - else error("AssetStore: Width for level '$level' not found!") - - // TODO: to be removed - fun getWorldHeight() : Float = assetLevelData.worldData.height.toFloat() - fun getWorldWidth(): Float = assetLevelData.worldData.width.toFloat() - - fun getWorldData(name: String) : AssetLevelData.WorldData = assetLevelData.worldData + // TODO change to array + fun getWorldData(name: String) : WorldData = assetLevelData.worldData fun getNinePatch(name: String) : NinePatchBmpSlice = if (images.contains(name)) { @@ -155,7 +127,7 @@ class AssetStore { // Update maps of music, images, ... assetConfig.tileMaps.forEach { tileMap -> val ldtkWorld = resourcesVfs[assetConfig.folder + "/" + tileMap.fileName].readLDTKWorld(extrude = true) - assetLevelData.loadLevelData(ldtkWorld, type, tileMap.hasParallax) + assetLevelData.loadLevelData(ldtkWorld) } assetConfig.sounds.forEach { sound -> @@ -229,6 +201,7 @@ class AssetStore { images.values.removeAll { it.first == type } fonts.values.removeAll { it.first == type } sounds.values.removeAll { it.first == type } - assetLevelData.removeAssets(type) + // TODO enable when level data is array again +// assetLevelData.values.removeAll { it.first == type } } } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/WorldData.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/WorldData.kt new file mode 100644 index 0000000..e90cfaa --- /dev/null +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/WorldData.kt @@ -0,0 +1,114 @@ +package korlibs.korge.fleks.assets + +import korlibs.image.bitmap.* +import korlibs.image.tiles.* + +/** + * + * @param tileSize - Size of a grid cell in pixels (e.g. 16 for 16x16 tile size) + * @param width - Width of whole level in pixels + * @param height - Height of whole level in pixels + */ +data class WorldData( + var tileSize: Int = 16, + + // Size of the whole world (in pixels) + var width: Float = 0f, + var height: Float = 0f, + var gridWidth: Int = 0, + var gridHeight: Int = 0, + + var layerlevelMaps: MutableMap = mutableMapOf(), +) { + + fun getLevelMap(layerName: String) : LevelMap { + if (!layerlevelMaps.contains(layerName)) println("WARNING: Level map for layer '$layerName' does not exist!") + return layerlevelMaps[layerName] ?: LevelMap(1, 1) + } +} + +data class LevelMap( + // Size of a level inside the grid vania array (all levels have the same size) + val gridWidth: Int, + val gridHeight: Int, + val entities: MutableList = mutableListOf(), // TODO change to list + var levelGridVania: List> = listOf(), +) { + /** + * Iterate over all tiles within the given view port area and call the renderCall function for each tile. + * + * @param x - horizontal position of top-left corner of view port in tiles + * @param y - vertical position of top-left corner of view port in tiles + * @param width - width of view port in tiles + * @param height - height of view port in tiles + */ + fun forEachTile(x: Int, y: Int, width: Int, height: Int, renderCall: (BmpSlice, Float, Float) -> Unit) { + // Calculate the view port corners (top-left, top-right, bottom-left and bottom-right positions) in gridvania indexes + // and check if the corners are in different level maps (tileMapData) + val gridX = x / gridWidth + val gridY = y / gridHeight + val gridX2 = (x + width) / gridWidth + val gridY2 = (y + height) / gridHeight + + val xStart = x % gridWidth + val yStart = y % gridHeight + + // Check if the view port area overlaps multiple levels + if (gridX == gridX2) { + // We have only one level in horizontal direction + if (gridY == gridY2) { + // We have only one level in vertical direction + processTiles(gridX, gridY, xStart, yStart, xStart + width, yStart + height, renderCall) + } else { + // We have vertically two levels + processTiles(gridX, gridY, xStart, yStart, xStart + width, gridHeight, renderCall) + processTiles(gridX, gridY2, xStart, 0, xStart + width, (yStart + height) % gridHeight, renderCall) + } + } else { + // We have horizontal two levels + if (gridY == gridY2) { + // We have only one level in vertical direction + processTiles(gridX, gridY, xStart, yStart, gridWidth, yStart + height, renderCall) + processTiles(gridX2, gridY, 0, yStart, (xStart + width) % gridWidth, yStart + height, renderCall) + } else { + // We have vertical two levels + processTiles(gridX, gridY, xStart, yStart, gridWidth, gridHeight, renderCall) + processTiles(gridX2, gridY, 0, yStart, (xStart + width) % gridWidth, gridHeight, renderCall) + processTiles(gridX, gridY2, xStart, 0, gridWidth, (yStart + height) % gridHeight, renderCall) + processTiles(gridX2, gridY2, 0, 0, (xStart + width) % gridWidth, (yStart + height) % gridHeight, renderCall) + } + } + } + + private fun processTiles(gridX: Int, gridY: Int, xStart: Int, yStart: Int, xEnd: Int, yEnd: Int, renderCall: (BmpSlice, Float, Float) -> Unit) { + levelGridVania[gridX][gridY].tileMapData?.let { tileMap -> + val tileSet = tileMap.tileSet + val tileWidth = tileSet.width + val tileHeight = tileSet.height + + for (l in 0 until tileMap.maxLevel) { + for (tx in xStart until xEnd) { + for (ty in yStart until yEnd) { + val tile = tileMap[tx, ty, l] + val tileInfo = tileSet.getInfo(tile.tile) + if (tileInfo != null) { + val px = (tx * tileWidth) + tile.offsetX + (gridX * gridWidth * tileWidth) + val py = (ty * tileHeight) + tile.offsetY + (gridY * gridHeight * tileWidth) + renderCall(tileInfo.slice, px.toFloat(), py.toFloat()) + } + } + } + } + } + + } +} + +// Data class for storing level data like grizSize, width, height, entities, tileMapData +data class LevelData( + var type: AssetType = AssetType.COMMON, // TODO: Remove + val gridSize: Int = 16, // TODO: Remove + + val entities: MutableList = mutableListOf(), + var tileMapData: TileMapData? = null +) 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 7bcdd19..70a662a 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 @@ -128,11 +128,11 @@ object GameStateManager { // TODO: Check if save game is available and load it // Load start script from level - val startScript = "${gameStateConfig.level}_start_script" + val startScript = "start_script" if (EntityFactory.contains(startScript)) { - println("INFO: Starting '${gameStateConfig.level}' with script: '$startScript'.") + println("INFO: Starting '${gameStateConfig.world}'") world.createAndConfigureEntity(startScript) } - else println("Error: Cannot start '${gameStateConfig.level}'! EntityConfig with name '$startScript' does not exist!") + else println("Error: Cannot start '${gameStateConfig.world}'! EntityConfig with name '$startScript' does not exist!") } } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LevelMapRenderSystem.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LevelMapRenderSystem.kt index 27f4570..77b5502 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LevelMapRenderSystem.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/renderSystems/LevelMapRenderSystem.kt @@ -18,8 +18,15 @@ inline fun Container.levelMapRenderSystem(world: World, layerTag: RenderLayerTag LevelMapRenderSystem(world, layerTag).addTo(this, callback) /** - * Here we do not render the actual level map yet. - * Instead, we add the view object for the level map to the container. + * RenderSystem to render level maps. It uses the [LevelMapComponent] to determine which level maps should be rendered + * and in which order. The [LayerComponent] is used to determine the rendering order of the level maps. + * The [RenderLayerTag] is used to determine the layer on which the level maps should be rendered. + * The [AssetStore] is used to retrieve the level maps and their data. + * The [EntityComparator] is used to sort the level maps by their layer index. + * + * @param world the world containing the entities to render + * @param layerTag the tag to determine the layer on which the level maps should be rendered + * @param comparator the comparator to sort the level maps by their layer index */ class LevelMapRenderSystem( private val world: World, @@ -30,16 +37,12 @@ class LevelMapRenderSystem( private val assetStore: AssetStore = world.inject(name = "AssetStore") - // Debugging layer rendering - private var renderLayer = 0 - override fun renderInternal(ctx: RenderContext) { val camera: Entity = world.getMainCamera() val cameraPosition = with(world) { camera[PositionComponent] } val cameraViewPort = with(world) { camera[SizeIntComponent] } val cameraViewPortHalf = with(world) { camera[SizeComponent] } - // Sort level maps by their layerIndex family.sort(comparator) @@ -47,62 +50,32 @@ class LevelMapRenderSystem( family.forEach { entity -> val (rgba) = entity[RgbaComponent] val (levelName, layerNames) = entity[LevelMapComponent] + val worldData = assetStore.getWorldData(levelName) - layerNames.forEach { layerName -> - val tileMap = assetStore.getTileMapData(levelName, layerName) - val worldData = assetStore.getWorldData(levelName) - val tileSet = tileMap.tileSet - val tileSetWidth = tileSet.width - val tileSetHeight = tileSet.height - val offsetScale = tileMap.offsetScale + // Calculate viewport position in world coordinates from Camera position (x,y) + offset + val viewPortPosX: Float = cameraPosition.x + cameraPosition.offsetX - cameraViewPortHalf.width + val viewPortPosY: Float = cameraPosition.y + cameraPosition.offsetY - cameraViewPortHalf.height - // Draw only visible tiles - // Calculate viewport position in world coordinates from Camera position (x,y) + offset - val viewPortX: Float = cameraPosition.x + cameraPosition.offsetX - cameraViewPortHalf.width - val viewPortY: Float = cameraPosition.y + cameraPosition.offsetY - cameraViewPortHalf.height + layerNames.forEach { layerName -> + val levelMap = worldData.getLevelMap(layerName) - // Start and end indexes of viewport area - val xStart: Int = viewPortX.toInt() / tileSetWidth - 1 // x in positive direction; -1 = start one tile before - val xTiles = (cameraViewPort.width / tileSetWidth) + 3 - val xEnd: Int = xStart + xTiles + // Start and end indexes of viewport area (in tile coordinates) + val xStart: Int = viewPortPosX.toInt() / worldData.tileSize - 1 // x in positive direction; -1 = start one tile before + val xTiles = (cameraViewPort.width / worldData.tileSize) + 3 - val yStart: Int = viewPortY.toInt() / tileSetHeight - 1 // y in negative direction; -1 = start one tile before - val yTiles = cameraViewPort.height / tileSetHeight + 3 - val yEnd: Int = yStart + yTiles + val yStart: Int = viewPortPosY.toInt() / worldData.tileSize - 1 // y in negative direction; -1 = start one tile before + val yTiles = cameraViewPort.height / worldData.tileSize + 3 ctx.useBatcher { batch -> - - - // 2. Check which levels the view port of the camera is touching - // TODO - - // Render one level - for (l in 0 until tileMap.maxLevel) { - // level is the "layer" from stacked tiles in ldtk - val level = - if (renderLayer == 0) l - else (renderLayer - 1).clamp(0, l) - - for (tx in xStart until xEnd) { - for (ty in yStart until yEnd) { - val tile = tileMap[tx, ty, level] - val info = tileSet.getInfo(tile.tile) - if (info != null) { - val px = (tx * tileSetWidth) + (tile.offsetX * offsetScale) - viewPortX - val py = (ty * tileSetHeight) + (tile.offsetY * offsetScale) - viewPortY - - batch.drawQuad( - tex = ctx.getTex(info.slice), - x = px, - y = py, - filtering = false, - colorMul = rgba, - program = null // Possibility to use a custom shader - add ShaderComponent or similar - ) - - } - } - } + levelMap.forEachTile(xStart, yStart, xTiles, yTiles) { slice, px, py -> + batch.drawQuad( + tex = ctx.getTex(slice), + x = px - viewPortPosX, + y = py - viewPortPosY, + filtering = false, + colorMul = rgba, + program = null // Possibility to use a custom shader - add ShaderComponent or similar + ) } } } @@ -118,20 +91,5 @@ class LevelMapRenderSystem( init { name = layerTag.toString() - - // For debugging layer rendering - keys { - justDown(Key.N0) { renderLayer = 0 } - justDown(Key.N1) { renderLayer = 1 } - justDown(Key.N2) { renderLayer = 2 } - justDown(Key.N3) { renderLayer = 3 } - justDown(Key.N4) { renderLayer = 4 } - justDown(Key.N5) { renderLayer = 5 } - justDown(Key.N6) { renderLayer = 6 } - justDown(Key.N7) { renderLayer = 7 } - justDown(Key.N8) { renderLayer = 8 } - justDown(Key.N9) { renderLayer = 9 } - } - } } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/CameraSystem.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/CameraSystem.kt index 3140efc..e4e3da4 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/CameraSystem.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/CameraSystem.kt @@ -10,7 +10,7 @@ import korlibs.korge.fleks.utils.* class CameraSystem( - worldToPixelRatio: Float + private val worldToPixelRatio: Float ) : IteratingSystem( family = family { all(CameraFollowTag) }, interval = EachFrame @@ -20,8 +20,9 @@ class CameraSystem( private val assetStore: AssetStore = inject("AssetStore") - private val worldHeight: Float = assetStore.getWorldHeight() - private val worldWidth: Float = assetStore.getWorldWidth() + // TODO how to get current world name? + private val worldHeight: Float = assetStore.getWorldData("").height + private val worldWidth: Float = assetStore.getWorldData("").width // These properties need to be set by the onAdd hook function of the ParallaxComponent var parallaxHeight: Float = 0f @@ -48,13 +49,17 @@ class CameraSystem( val newCameraPositionY = cameraPosition.y + yDiff * factor // Keep camera within world bounds + val leftBound = viewPortHalf.width + worldToPixelRatio + val rightBound = worldWidth - viewPortHalf.width - worldToPixelRatio + val topBound = viewPortHalf.height + worldToPixelRatio + val bottomBound = worldHeight - viewPortHalf.height - worldToPixelRatio cameraPosition.x = - if (newCameraPositionX < viewPortHalf.width) viewPortHalf.width - else if (newCameraPositionX > worldWidth - viewPortHalf.width) worldWidth - viewPortHalf.width + if (newCameraPositionX < leftBound) leftBound + else if (newCameraPositionX > rightBound) rightBound else newCameraPositionX cameraPosition.y = - if (newCameraPositionY < viewPortHalf.height) viewPortHalf.height - else if (newCameraPositionY > worldHeight - viewPortHalf.height) worldHeight - viewPortHalf.height + if (newCameraPositionY < topBound) topBound + else if (newCameraPositionY > bottomBound) bottomBound else newCameraPositionY // Move parallax layers if camera moves