Skip to content

Commit

Permalink
Add World to Screen Coordinate translation for game objects (#36)
Browse files Browse the repository at this point in the history
- Add camera entity for render systems which contains camera position component
- Introduce ScreenCoordinatesTag for distinguishing world vs sceen coordinates
- Update SnapshotSerializer and EntityConfigSerializer modules
  • Loading branch information
jobe-m authored Jan 7, 2025
1 parent b243eb9 commit 2dbdd0e
Show file tree
Hide file tree
Showing 25 changed files with 166 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import kotlinx.serialization.Serializable
@Serializable @SerialName("Info")
data class InfoComponent(
var name: String = "noName",
var ldtkIdentifier: String = "",
var entityId: Int = -1,

// internal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package korlibs.korge.fleks.components

import com.github.quillraven.fleks.*
import korlibs.datastructure.iterators.*
import korlibs.image.color.*
import korlibs.image.format.*
import korlibs.image.format.ImageAnimation.Direction.*
import korlibs.korge.fleks.assets.*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package korlibs.korge.fleks.components

import com.github.quillraven.fleks.*
import korlibs.korge.fleks.systems.*
import korlibs.korge.fleks.utils.*
import korlibs.korge.fleks.components.TweenPropertyComponent.*
import korlibs.math.interpolation.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ data class SpriteComponent(
var animation: String? = null, // Leave null if sprite texture does not have an animation
var frameIndex: Int = 0, // frame number of animation which is currently drawn
var running: Boolean = false, // Switch animation on and off
var direction: ImageAnimation.Direction? = null,
var direction: ImageAnimation.Direction? = null, // Default: Get direction from Aseprite file
var destroyOnAnimationFinished: Boolean = false, // Delete entity when direction is [ONCE_FORWARD] or [ONCE_REVERSE]

// internal, do not set directly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,6 @@ data class TweenSequenceComponent(
)
}


@Serializable @SerialName("TweenMotion")
data class TweenMotion(
var velocityX: Float? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ object EntityFactory {

val entityConfig = entityConfigs[name]
return if (entityConfig != null) {
// println("INFO: Configure entity '${baseEntity.id}' with '${entityConfig.name}' EntityConfig.")
// println("INFO: Configure entity '${baseEntity.id}' with '${entityConfig.name}' EntityConfig.")
entityConfig.run { world.entityConfigure(baseEntity) }
} else {
println("WARNING: Cannot invoke! EntityConfig with name '$name' not registered in EntityFactory!")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package korlibs.korge.fleks.entity.config


import com.github.quillraven.fleks.*
import korlibs.korge.fleks.components.*
import korlibs.korge.fleks.entity.*
import korlibs.korge.fleks.utils.*
import kotlinx.serialization.*

@Serializable @SerialName("CameraConfig")
data class CameraConfig(
override val name: String
) : EntityConfig {

override fun World.entityConfigure(entity: Entity) : Entity {

entity.configure {
// Camera has position within the game world
// Offset can be used to "shake" the camera on explosions etc.
it += PositionComponent()

// TODO: Add bounds of level world
}
return entity
}

init {
EntityFactory.register(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ data class DialogBoxConfig(
}
// Avatar image entity
val avatar = entity {
it += ScreenCoordinatesTag
it += PositionComponent(x = avatarInitialX, y = textBoxY - 24f)
it += SpriteComponent(name = avatarName)
it += RgbaComponent().apply {
Expand All @@ -103,6 +104,7 @@ data class DialogBoxConfig(
// it += RenderLayerTag.DEBUG
}
val textBox = entity {
it += ScreenCoordinatesTag
it += PositionComponent(x = textBoxX, y = textBoxY)
it += NinePatchComponent(
name = textFieldName,
Expand All @@ -118,6 +120,7 @@ data class DialogBoxConfig(
// it += RenderLayerTag.DEBUG
}
val textField = entity {
it += ScreenCoordinatesTag
it += PositionComponent(x = textFieldX, y = textFieldY)
it += TextFieldComponent(
text = text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ data class FireAndDustEffectConfig(

private val renderLayerTag: RenderLayerTag = RenderLayerTag.MAIN_EFFECTS,
private val layerIndex: Int? = null,
private val fadeOutDuration: Float = 0f
private val fadeOutDuration: Float = 0f,
private val screenCoordinates: Boolean = false
) : EntityConfig {

// Configure function which applies the config to the entity's components
override fun World.entityConfigure(entity: Entity) : Entity {
entity.configure {
if (screenCoordinates) it += ScreenCoordinatesTag
it += InfoComponent(name = this@FireAndDustEffectConfig.name)

var velocityXX = velocityX
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ data class LevelMapConfig(
override fun World.entityConfigure(entity: Entity) : Entity {
entity.configure {
it += LevelMapComponent(levelName, layerNames)
it += PositionComponent(
x = x,
y = y
)
// Level map does not have position - camera position will determine what is shown from the level map
it += SizeComponent() // Size of level map needs to be set after loading of map is finished
// TODO: Check if SizeComponent is needed because it is static and does not change
it += RgbaComponent().apply {
alpha = this@LevelMapConfig.alpha
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import korlibs.korge.fleks.components.*
import korlibs.korge.fleks.entity.*
import korlibs.korge.fleks.tags.*
import korlibs.korge.fleks.utils.*
import korlibs.math.geom.*
import kotlinx.serialization.*


Expand All @@ -21,8 +22,6 @@ data class LogoEntityConfig(
override val name: String,

private val assetName: String,
private val viewPortWidth: Int = 0,
private val viewPortHeight: Int = 0,
private val centerX: Boolean = false,
private val centerY: Boolean = false,
private val offsetX: Float = 0f,
Expand All @@ -36,11 +35,12 @@ data class LogoEntityConfig(

override fun World.entityConfigure(entity: Entity) : Entity {
val assetStore: AssetStore = inject(name = "AssetStore")
val viewPortSize: SizeInt = inject(name = "ViewPortSize")

entity.configure {
it += PositionComponent(
x = offsetX + (if (centerX) (viewPortWidth - assetStore.getImageData(assetName).width).toFloat() * 0.5f else 0f),
y = offsetY + (if (centerY) (viewPortHeight - assetStore.getImageData(assetName).height).toFloat() * 0.5f else 0f)
x = offsetX + (if (centerX) (viewPortSize.width - assetStore.getImageData(assetName).width).toFloat() * 0.5f else 0f),
y = offsetY + (if (centerY) (viewPortSize.height - assetStore.getImageData(assetName).height).toFloat() * 0.5f else 0f)
)
it += LayeredSpriteComponent(
name = assetName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ data class RichTextConfig(
// Position and size of text field
private val x: Float = 0f,
private val y: Float = 0f,
private val screenCoordinates: Boolean = false,
private val width: Float = 0f, // width and height is used only for alignment of the text
private val height: Float = 0f,
private val textRangeEnd: Int = Int.MAX_VALUE,
Expand All @@ -37,6 +38,7 @@ data class RichTextConfig(

override fun World.entityConfigure(entity: Entity) : Entity {
entity.configure {
if (screenCoordinates) it += ScreenCoordinatesTag
it += PositionComponent(
x = this@RichTextConfig.x,
y = this@RichTextConfig.y
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ object GameStateManager {
}

/**
* This function is called to start the game. It will load the start script from the current LDtk level and
* This function is called to start the game. It will load the "start script" from the current LDtk level and
* create and configure the very first entity of the game.
*
* Hint: Make sure that a game object with name "start_script" is present in each LDtk level. It can be of dirrerent
* Hint: Make sure that a game object with name "start_script" is present in each LDtk level. It can be of different
* EntityConfig type. The type can be set in LDtk level map editor.
*/
fun startGame(world: World) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import korlibs.datastructure.iterators.*
import korlibs.image.color.*
import korlibs.korge.fleks.assets.*
import korlibs.korge.fleks.components.*
import korlibs.korge.fleks.tags.RenderLayerTag
import korlibs.korge.fleks.tags.*
import korlibs.korge.render.*
import korlibs.korge.view.*
import korlibs.math.geom.*
Expand All @@ -15,26 +15,45 @@ import korlibs.math.geom.Point
/**
* Creates a new [DebugRenderSystem], allowing to configure with [callback], and attaches the newly created view to the
* receiver this */
inline fun Container.debugRenderSystem(viewPortSize: SizeInt, world: World, layerTag: RenderLayerTag, callback: @ViewDslMarker DebugRenderSystem.() -> Unit = {}) =
DebugRenderSystem(viewPortSize, world, layerTag).addTo(this, callback)
inline fun Container.debugRenderSystem(viewPortSize: SizeInt, camera: Entity, world: World, layerTag: RenderLayerTag, callback: @ViewDslMarker DebugRenderSystem.() -> Unit = {}) =
DebugRenderSystem(viewPortSize,camera, world, layerTag).addTo(this, callback)

class DebugRenderSystem(
private val viewPortSize: SizeInt,
private val camera: Entity,
world: World,
private val layerTag: RenderLayerTag
) : View() {
private val family: Family = world.family {
all(layerTag, PositionComponent)
all(layerTag)
.any(PositionComponent, SpriteComponent, LayeredSpriteComponent, TextFieldComponent, NinePatchComponent)
}

private val assetStore: AssetStore = world.inject(name = "AssetStore")
private val viewPortHalfWidth: Int = viewPortSize.width / 2
private val viewPortHalfHeight: Int = viewPortSize.height / 2

override fun renderInternal(ctx: RenderContext) {
// Custom Render Code here
ctx.useLineBatcher { batch ->
family.forEach { entity ->
val (x, y, offsetX, offsetY) = entity[PositionComponent]
val (entityX, entityY, entityOffsetX, entityOffsetY) = entity[PositionComponent]
val x: Float
val y: Float
val offsetX: Float = entityOffsetX
val offsetY: Float = entityOffsetY

if (entity has ScreenCoordinatesTag) {
// Take over entity coordinates
x = entityX
y = entityY
} else {
// Transform world coordinates to screen coordinates
val cameraPosition = camera[PositionComponent]
x = entityX - cameraPosition.x + viewPortHalfWidth + cameraPosition.offsetX
y = entityY - cameraPosition.y + viewPortHalfHeight + cameraPosition.offsetY
}

val xx: Float = x + offsetX
val yy: Float = y + offsetY

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@ import korlibs.math.*
import korlibs.math.geom.*


inline fun Container.levelMapRenderSystem(viewPortSize: SizeInt, world: World, layerTag: RenderLayerTag, callback: @ViewDslMarker LevelMapRenderSystem.() -> Unit = {}) =
LevelMapRenderSystem(viewPortSize, world, layerTag).addTo(this, callback)
inline fun Container.levelMapRenderSystem(viewPortSize: SizeInt, camera: Entity, world: World, layerTag: RenderLayerTag, callback: @ViewDslMarker LevelMapRenderSystem.() -> Unit = {}) =
LevelMapRenderSystem(viewPortSize, camera, 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.
*/
class LevelMapRenderSystem(
private val viewPortSize: SizeInt,
private val camera: Entity,
world: World,
layerTag: RenderLayerTag,
private val comparator: EntityComparator = compareEntity(world) { entA, entB -> entA[LayerComponent].layerIndex.compareTo(entB[LayerComponent].layerIndex) }
) : View() {
private val family: Family = world.family { all(layerTag, LayerComponent, PositionComponent, LevelMapComponent) }
private val family: Family = world.family { all(layerTag, LayerComponent, LevelMapComponent) }
private val assetStore: AssetStore = world.inject(name = "AssetStore")
private val viewPortHalfWidth: Int = viewPortSize.width / 2
private val viewPortHalfHeight: Int = viewPortSize.height / 2

// Debugging layer rendering
private var renderLayer = 0
Expand All @@ -39,11 +42,10 @@ class LevelMapRenderSystem(

// Iterate over all entities which should be rendered in this view
family.forEach { entity ->
val (x, y, offsetX, offsetY) = entity[PositionComponent]
val (cameraX, cameraY, cameraOffsetX, cameraOffsetY) = camera[PositionComponent]
val (rgba) = entity[RgbaComponent]
val (levelName, layerNames) = entity[LevelMapComponent]

val rgba = Colors.WHITE // TODO: use here alpha from ldtk layer

layerNames.forEach { layerName ->
val tileMap = assetStore.getTileMapData(levelName, layerName)
val tileSet = tileMap.tileSet
Expand All @@ -52,12 +54,16 @@ class LevelMapRenderSystem(
val offsetScale = tileMap.offsetScale

// Draw only visible tiles
// Start and end need to be calculated from Camera viewport position (x,y) + offset
val xStart: Int = -((x + offsetX).toInt() / tileSetWidth) - 1 // x in positive direction; -1 = start one tile before
// Calculate viewport position in world coordinates from Camera position (x,y) + offset
val viewPortX: Float = cameraX + cameraOffsetX - viewPortHalfWidth
val viewPortY: Float = cameraY + cameraOffsetY - viewPortHalfHeight

// 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 = (viewPortSize.width / tileSetWidth) + 3
val xEnd: Int = xStart + xTiles

val yStart: Int = -((y + offsetY).toInt() / tileSetHeight) - 1 // y in negative direction; -1 = start one tile before
val yStart: Int = viewPortY.toInt() / tileSetHeight - 1 // y in negative direction; -1 = start one tile before
val yTiles = viewPortSize.height / tileSetHeight + 3
val yEnd: Int = yStart + yTiles

Expand All @@ -72,17 +78,16 @@ class LevelMapRenderSystem(
val tile = tileMap[tx, ty, level]
val info = tileSet.getInfo(tile.tile)
if (info != null) {
val px = x + offsetX + (tx * tileSetWidth) + (tile.offsetX * offsetScale)
val py = y + offsetY + (ty * tileSetHeight) + (tile.offsetY * offsetScale)
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,
// TODO: Add possibility to use a custom shader - add ShaderComponent or similar
program = null
program = null // Possibility to use a custom shader - add ShaderComponent or similar
)

}
Expand Down
Loading

0 comments on commit 2dbdd0e

Please sign in to comment.