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 9d27d60..69fa1f7 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,8 +1,8 @@ package korlibs.korge.fleks.assets -import korlibs.image.bitmap.* import korlibs.image.tiles.* import korlibs.korge.fleks.utils.* +import korlibs.korge.fleks.assets.WorldData.* import korlibs.korge.ldtk.* import korlibs.korge.ldtk.view.* import korlibs.math.* @@ -10,14 +10,15 @@ import korlibs.memory.* import kotlinx.serialization.* import kotlin.math.* +/** + * Data class for storing level maps and entities for a game world. + */ class AssetLevelData { - internal val worldData = WorldData() internal val configDeserializer = EntityConfigSerializer() - private var gameObjectCnt = 0 - // Size of a level within the grid vania array in grid coordinates (index) + // Size of a level within the gridvania array in pixels private var gridVaniaWidth: Int = 0 private var gridVaniaHeight: Int = 0 @@ -44,12 +45,20 @@ class AssetLevelData { } // Create grid vania array - worldData.gridWidth = (maxLevelOffsetX / gridVaniaWidth) + 1 - worldData.gridHeight = (maxLevelOffsetY / gridVaniaHeight) + 1 + val gridWidth = (maxLevelOffsetX / gridVaniaWidth) + 1 + val gridHeight = (maxLevelOffsetY / gridVaniaHeight) + 1 // Set the size of the world - worldData.width = (worldData.gridWidth * gridVaniaWidth).toFloat() - worldData.height = (worldData.gridHeight * gridVaniaHeight).toFloat() + worldData.width = (gridWidth * gridVaniaWidth).toFloat() + worldData.height = (gridHeight * gridVaniaHeight).toFloat() + + // Set the size of a grid cell in pixels + worldData.gridSize = ldtkWorld.ldtk.defaultGridSize + + // Set the size of a level in the grid vania array + worldData.levelWidth = gridVaniaWidth / worldData.gridSize + worldData.levelHeight = gridVaniaHeight / worldData.gridSize + if (maxLevelOffsetX == 0) println("WARNING: Level width is 0!") @@ -59,7 +68,7 @@ class AssetLevelData { val levelY: Int = ldtkLevel.worldY / gridVaniaHeight loadLevel(worldData, levelX, levelY, ldtkWorld, ldtkLevel) } - println("Gridvania size: ${worldData.gridWidth} x ${worldData.gridHeight})") + println("Gridvania size: ${gridWidth} x ${gridHeight})") } fun reloadAsset(ldtkWorld: LDTKWorld) { @@ -73,23 +82,11 @@ class AssetLevelData { 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 + val entities: MutableList = mutableListOf() - // 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()) { @@ -98,7 +95,6 @@ class AssetLevelData { 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") @@ -107,13 +103,9 @@ class AssetLevelData { 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 = (level position in the world) + (position within the level) + (pivot point) + val entityPosX: Int = (gridVaniaWidth * levelX) + (entity.gridPos.x * gridSize) + (entity.pivot[0] * gridSize).toInt() + val entityPosY: Int = (gridVaniaHeight * levelY) + (entity.gridPos.y * gridSize) + (entity.pivot[1] * gridSize).toInt() // Add position of entity entity.tags.firstOrNull { it == "positionable" }?.let { @@ -125,20 +117,17 @@ class AssetLevelData { 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") + //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'") + // We need to store only the name of the entity config for later dynamically spawning of entities + entities.add(entityConfig.name) + //println("INFO: Registering entity config '${entity.identifier}' for '$levelName'") } catch (e: Throwable) { println("ERROR: Loading entity config - $e") } @@ -147,13 +136,26 @@ class AssetLevelData { } } - // Check if layer has tile set -> store tile map data - val tilesetExt = ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] - if (tilesetExt != null) { - storeTiles(levelData, ldtkLayer, tilesetExt) + if (!worldData.layerlevelMaps.contains(layerName)) { + // We store the entity configs in a 2D array depending on its gridvania position in the world + // Then later we will spawn the entities depending on the level which the player is currently in + val gridWidth = (worldData.width.toInt() / gridVaniaWidth) + 1 // +1 for guard needed in world map renderer + val gridHeight = (worldData.height.toInt() / gridVaniaHeight) + 1 + worldData.layerlevelMaps[layerName] = LevelMap(levelGridVania = List(gridWidth) { List(gridHeight) { LevelData() } }) + } + + val levelData = worldData.layerlevelMaps[layerName]!!.levelGridVania[levelX][levelY] + + if (entities.isNotEmpty()) { + // Layer has entities -> store entity data - no tile data + levelData.entities = entities + } else if (ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid] != null) { + // Layer has tile set -> store tile map data - no entity data + storeTiles(levelData, ldtkLayer, ldtkWorld.tilesetDefsById[ldtkLayer.tilesetDefUid]!!) + } else { + println("WARNING: Layer '$layerName' of level '$levelName' has no tile set or entities!") } } - } private fun storeTiles(levelData: LevelData, ldtkLayer: LayerInstance, tilesetExt: ExtTileset) { 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 5b32738..f783fe3 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 @@ -34,5 +34,3 @@ data class AssetModel( val hasParallax: Boolean = true ) } - -enum class AssetType { COMMON, WORLD, LEVEL, SPECIAL } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetType.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetType.kt new file mode 100644 index 0000000..24aed1b --- /dev/null +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/AssetType.kt @@ -0,0 +1,3 @@ +package korlibs.korge.fleks.assets + +enum class AssetType { COMMON, WORLD, LEVEL, SPECIAL } 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 index e90cfaa..fdb2c34 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/WorldData.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/WorldData.kt @@ -4,111 +4,107 @@ import korlibs.image.bitmap.* import korlibs.image.tiles.* /** + * Data class for storing level maps and entities for a game world. * - * @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 + * @param width - Width of whole world in pixels + * @param height - Height of whole world in pixels */ data class WorldData( - var tileSize: Int = 16, - - // Size of the whole world (in pixels) + // Size of all gridvania levels in the world (in pixels / world coordinates) var width: Float = 0f, var height: Float = 0f, - var gridWidth: Int = 0, - var gridHeight: Int = 0, - + // Size of a level inside the grid vania array in tiles (all levels have the same size) + var levelWidth: Int = 0, + var levelHeight: Int = 0, + // Size of a grid cell in pixels (e.g. 16 for 16x16 tile size) + var gridSize: Int = 1, var layerlevelMaps: MutableMap = mutableMapOf(), ) { - fun getLevelMap(layerName: String) : LevelMap { + 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) + return layerlevelMaps[layerName] ?: LevelMap() } -} -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 + data class LevelMap( + val 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 + * @param levelWidth - width of a level in tiles + * @param levelHeight - height of a level in tiles + */ + fun forEachTile(x: Int, y: Int, width: Int, height: Int, levelWidth: Int, levelHeight: 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 / levelWidth + val gridY = y / levelHeight + val gridX2 = (x + width) / levelWidth + val gridY2 = (y + height) / levelHeight - val xStart = x % gridWidth - val yStart = y % gridHeight + val xStart = x % levelWidth + val yStart = y % levelHeight - // 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) + // 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, levelWidth, levelHeight, renderCall) + } else { + // We have vertically two levels + processTiles(gridX, gridY, xStart, yStart, xStart + width, levelHeight, levelWidth, levelHeight, renderCall) + processTiles(gridX, gridY2, xStart, 0, xStart + width, (yStart + height) % levelHeight, levelWidth, levelHeight, 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) + // We have horizontal two levels + if (gridY == gridY2) { + // We have only one level in vertical direction + processTiles(gridX, gridY, xStart, yStart, levelWidth, yStart + height, levelWidth, levelHeight, renderCall) + processTiles(gridX2, gridY, 0, yStart, (xStart + width) % levelWidth, yStart + height, levelWidth, levelHeight, renderCall) + } else { + // We have vertical two levels + processTiles(gridX, gridY, xStart, yStart, levelWidth, levelHeight, levelWidth, levelHeight, renderCall) + processTiles(gridX2, gridY, 0, yStart, (xStart + width) % levelWidth, levelHeight, levelWidth, levelHeight, renderCall) + processTiles(gridX, gridY2, xStart, 0, levelWidth, (yStart + height) % levelHeight, levelWidth, levelHeight, renderCall) + processTiles(gridX2, gridY2, 0, 0, (xStart + width) % levelWidth, (yStart + height) % levelHeight, levelWidth, levelHeight, 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 + private fun processTiles(gridX: Int, gridY: Int, xStart: Int, yStart: Int, xEnd: Int, yEnd: Int, levelWidth: Int, levelHeight: 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()) + 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 * levelWidth * tileWidth) + val py = (ty * tileHeight) + tile.offsetY + (gridY * levelHeight * tileHeight) + 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 -) + /** + * Data class for storing level data like entities and tileMapData + */ + data class LevelData( + var entities: List = listOf(), + var tileMapData: TileMapData? = null + ) +} diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/ParallaxEffectConfig.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/ParallaxEffectConfig.kt index 897719a..e5a508f 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/ParallaxEffectConfig.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/ParallaxEffectConfig.kt @@ -27,7 +27,7 @@ data class ParallaxEffectConfig( y = this@ParallaxEffectConfig.y ) it += MotionComponent( - velocityX = -12f // world units (16 pixels) per second (??? TODO: this needs to be ckecked) + velocityX = -12f // world units (16 pixels) per second ) it += ParallaxComponent( name = assetName 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 77b5502..20c2e8b 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 @@ -51,6 +51,7 @@ class LevelMapRenderSystem( val (rgba) = entity[RgbaComponent] val (levelName, layerNames) = entity[LevelMapComponent] val worldData = assetStore.getWorldData(levelName) + val tileSize = worldData.gridSize // Calculate viewport position in world coordinates from Camera position (x,y) + offset val viewPortPosX: Float = cameraPosition.x + cameraPosition.offsetX - cameraViewPortHalf.width @@ -60,14 +61,14 @@ class LevelMapRenderSystem( val levelMap = worldData.getLevelMap(layerName) // 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 xStart: Int = viewPortPosX.toInt() / tileSize - 1 // x in positive direction; -1 = start one tile before + val xTiles = (cameraViewPort.width / tileSize) + 3 - val yStart: Int = viewPortPosY.toInt() / worldData.tileSize - 1 // y in negative direction; -1 = start one tile before - val yTiles = cameraViewPort.height / worldData.tileSize + 3 + val yStart: Int = viewPortPosY.toInt() / tileSize - 1 // y in negative direction; -1 = start one tile before + val yTiles = cameraViewPort.height / tileSize + 3 ctx.useBatcher { batch -> - levelMap.forEachTile(xStart, yStart, xTiles, yTiles) { slice, px, py -> + levelMap.forEachTile(xStart, yStart, xTiles, yTiles, worldData.levelWidth, worldData.levelHeight) { slice, px, py -> batch.drawQuad( tex = ctx.getTex(slice), x = px - viewPortPosX, 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 e4e3da4..98aa7a0 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 @@ -48,7 +48,7 @@ class CameraSystem( val newCameraPositionX = cameraPosition.x + xDiff * factor val newCameraPositionY = cameraPosition.y + yDiff * factor - // Keep camera within world bounds + // Keep camera within world bounds (+1 tile in each direction as guard for shaking camera - via camera offset) val leftBound = viewPortHalf.width + worldToPixelRatio val rightBound = worldWidth - viewPortHalf.width - worldToPixelRatio val topBound = viewPortHalf.height + worldToPixelRatio