Skip to content

Commit

Permalink
Cleanup and refactor world data assets and map renderer (#48)
Browse files Browse the repository at this point in the history
- Cleanup and refactor world data assets and map renderer
- Fix entity position in world coordinates
- Remove todo
  • Loading branch information
jobe-m authored Feb 24, 2025
1 parent 92c0a68 commit 601ddbf
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 135 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
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.*
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

Expand All @@ -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!")

Expand All @@ -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) {
Expand All @@ -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<String> = 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()) {
Expand All @@ -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")
Expand All @@ -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 {
Expand All @@ -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")
}
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,3 @@ data class AssetModel(
val hasParallax: Boolean = true
)
}

enum class AssetType { COMMON, WORLD, LEVEL, SPECIAL }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package korlibs.korge.fleks.assets

enum class AssetType { COMMON, WORLD, LEVEL, SPECIAL }
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, LevelMap> = 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<String> = mutableListOf(), // TODO change to list
var levelGridVania: List<List<LevelData>> = 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<List<LevelData>> = 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<String> = mutableListOf(),
var tileMapData: TileMapData? = null
)
/**
* Data class for storing level data like entities and tileMapData
*/
data class LevelData(
var entities: List<String> = listOf(),
var tileMapData: TileMapData? = null
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 601ddbf

Please sign in to comment.