From 8c8318b7eb59ec1a75d2eeaf55d72198060684ca Mon Sep 17 00:00:00 2001 From: jobeGameDev Date: Mon, 17 Feb 2025 13:27:28 +0100 Subject: [PATCH] Fix scrolling of parallax layer background within game world (#46) --- .../korge/fleks/assets/ParallaxConfigExt.kt | 27 ++++++++++++++----- .../fleks/components/ParallaxComponent.kt | 5 ++-- .../korge/fleks/entity/config/CameraConfig.kt | 6 ++--- .../korge/fleks/systems/CameraSystem.kt | 17 ++++++------ .../korge/fleks/systems/ParallaxSystem.kt | 12 ++++----- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/ParallaxConfigExt.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/ParallaxConfigExt.kt index d879422..0d7e49c 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/ParallaxConfigExt.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/assets/ParallaxConfigExt.kt @@ -7,9 +7,19 @@ import korlibs.image.format.* import korlibs.io.file.* import korlibs.korge.fleks.utils.* import kotlinx.serialization.* -import kotlinx.serialization.EncodeDefault.Mode.NEVER - +import korlibs.korge.fleks.renderSystems.ParallaxRenderSystem +/** This function reads the parallax data from an Aseprite file. + * It reads the background, foreground and attached layers as well as the parallax plane from the Aseprite file. + * + * The [config] object contains the configuration for the parallax effect. It contains the name of the Aseprite file + * and the configuration for the layers and the parallax plane. + * + * The [format] parameter is used to specify the image format of the file. Currently only Aseprite is supported [ASE]. + * + * The [atlas] parameter is used to pack the image data into an atlas. If it is not null the image data will be packed + * into the atlas. If it is null the image data will be stored separately. + */ suspend fun VfsFile.readParallaxDataContainer( config: ParallaxConfig, format: ImageFormat = ASE, @@ -89,11 +99,11 @@ suspend fun VfsFile.readParallaxDataContainer( when (config.mode) { ParallaxConfig.Mode.HORIZONTAL_PLANE -> { (backgroundLayers?.height ?: foregroundLayers?.height ?: attachedLayersFront?.height - ?: attachedLayersRear?.height ?: 0) - (config.parallaxPlane?.offset ?: 0) + ?: attachedLayersRear?.height ?: 0) - config.offset } ParallaxConfig.Mode.VERTICAL_PLANE -> { (backgroundLayers?.width ?: foregroundLayers?.width ?: attachedLayersFront?.width - ?: attachedLayersRear?.height ?: 0) - (config.parallaxPlane?.offset ?: 0) + ?: attachedLayersRear?.height ?: 0) - config.offset } ParallaxConfig.Mode.NO_PLANE -> 0 // not used without parallax plane setup } @@ -119,7 +129,8 @@ suspend fun VfsFile.readParallaxDataContainer( } /** - * This class contains all data which is needed by the [ParallaxDataView] to display the parallax view on the screen. + * This class contains all data which is needed by [ParallaxRenderSystem] to render parallax data from an Aseprite file. + * * It stores the [ParallaxConfig] and all [ImageData] objects for the background, foreground and attached Layers. The * parallax plane is a sliced Aseprite image and therefore consists of a [ImageDataContainer] object. * @@ -137,9 +148,12 @@ data class ParallaxDataContainer( ) /** * This is the main parallax configuration. + * * The [aseName] is the name of the aseprite file which is used for reading the image data. * (Currently it is not used. It will be used when reading the config from YAML/JSON file.) * + * [offset] is the amount of pixels from the top of the image where the upper part of the parallax plane starts. + * * The parallax [mode] has to be one of the following enum values: * - [NO_PLANE] * This type is used to set up a parallax background which will scroll repeatedly in X and Y direction. For this @@ -161,6 +175,7 @@ data class ParallaxDataContainer( @Serializable @SerialName("ParallaxConfig") data class ParallaxConfig( val aseName: String, + val offset: Int = 0, val mode: Mode = Mode.HORIZONTAL_PLANE, val backgroundLayers: ArrayList? = null, val parallaxPlane: ParallaxPlaneConfig? = null, @@ -178,7 +193,6 @@ data class ParallaxConfig( * The top part is the upper half of the Aseprite image. The bottom part is the bottom half of the image. This is used * to simulate a central vanishing point in the resulting parallax effect. * - * [offset] is the amount of pixels from the top of the image where the upper part of the parallax plane starts. * [name] has to be set to the name of the layer in the Aseprite which contains the image for the sliced stripes * of the parallax plane. * [speedFactor] is the factor for scrolling the parallax plane relative to the game play field (which usually contains the @@ -191,7 +205,6 @@ data class ParallaxConfig( */ @Serializable @SerialName("ParallaxPlaneConfig") data class ParallaxPlaneConfig( - val offset: Int = 0, val name: String, val speedFactor: Float = 1f, val selfSpeed: Float = 0f, diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/components/ParallaxComponent.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/components/ParallaxComponent.kt index 28b3749..b7d5b67 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/components/ParallaxComponent.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/components/ParallaxComponent.kt @@ -125,8 +125,9 @@ data class ParallaxComponent( parallaxDataContainer.foregroundLayers?.height ?: parallaxDataContainer.parallaxPlane?.default?.height ?: throw Error("ParallaxComponent: Parallax image data has no height!") ).toFloat() - val parallaxLayerHeight: Float = imageHeight - (parallaxDataContainer.config.parallaxPlane?.offset ?: 0) - this.system().parallaxHeight = parallaxLayerHeight + val parallaxLayerHeight: Float = imageHeight + this.system().parallaxHeight = parallaxLayerHeight - parallaxDataContainer.config.offset.toFloat() + this.system().parallaxOffset = parallaxDataContainer.config.offset.toFloat() } /** diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/CameraConfig.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/CameraConfig.kt index f332f40..6b08e29 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/CameraConfig.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/entity/config/CameraConfig.kt @@ -22,6 +22,9 @@ data class MainCameraConfig( // Camera has position within the game world // Offset can be used to "shake" the camera on explosions etc. it += PositionComponent() + + // TODO: Move view port size to injectable object - it is dependent on the target device and game config might be + // moved to another device with different screen size // Camera has a size which is the view port of the game it += SizeIntComponent( width = viewPortWith, // SizeIntComponent is used to store the view port size as integer values @@ -35,9 +38,6 @@ data class MainCameraConfig( // Camera has a tag to make it easily accessible for other systems and entity configurations it += MainCameraTag - - // TODO: Add bounds of level world (really here?) - } return entity } 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 35aa512..3140efc 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 @@ -18,12 +18,14 @@ class CameraSystem( private val worldToPixelRatioInv = 1f / worldToPixelRatio private val factor = 0.05f - private val assetStore: AssetStore = inject("AssetStore") + private val assetStore: AssetStore = inject("AssetStore") private val worldHeight: Float = assetStore.getWorldHeight() private val worldWidth: Float = assetStore.getWorldWidth() + // These properties need to be set by the onAdd hook function of the ParallaxComponent var parallaxHeight: Float = 0f + var parallaxOffset: Float = 0f override fun onTickEntity(entity: Entity) { @@ -65,20 +67,19 @@ class CameraSystem( val position = parallaxEntity[PositionComponent] val viewPortHeight = camera[SizeIntComponent].height - // Convert pixel distance of camera movement in the level to velocity for parallax layers + // Convert pixel distance of camera movement in the level to horizontal velocity for parallax layers val distanceInWorldUnits = cameraDistX * worldToPixelRatioInv // (distance in pixel) / (world to pixel ratio) motion.velocityX = -distanceInWorldUnits / deltaTime // world units per delta-time + // Calculate the ratio of the camera position in the world to the world height // Camera position is in world coordinates - val ratio = cameraPosition.y / worldHeight // range: [0...1] + val ratio = (cameraPosition.y - viewPortHalf.height) / (worldHeight - viewPortHeight) // range: [0...1] - // TODO: Get vertical parallax offset from parallax config - val parallaxOffset = -36f // Get the global position of the parallax layer in screen coordinates - val parallaxVerticalMax = viewPortHeight - parallaxHeight - parallaxOffset - val parallaxVerticalPosition = ratio * parallaxVerticalMax + parallaxOffset + val parallaxVerticalLength = viewPortHeight - parallaxHeight + val parallaxVerticalPosition = ratio * parallaxVerticalLength - position.y = parallaxVerticalPosition + position.y = parallaxVerticalPosition - parallaxOffset // println("parallax y: $position.y") } } diff --git a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/ParallaxSystem.kt b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/ParallaxSystem.kt index db6547b..7e67e36 100644 --- a/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/ParallaxSystem.kt +++ b/korge-fleks/src/commonMain/kotlin/korlibs/korge/fleks/systems/ParallaxSystem.kt @@ -20,6 +20,7 @@ class ParallaxSystem( val (configName, backgroundLayers, parallaxPlane, foregroundLayers) = entity[ParallaxComponent] val (_, _, velocityX, velocityY) = entity[MotionComponent] val parallaxDataContainer = assetStore.getBackground(configName) + val offset = parallaxDataContainer.config.offset // Update local positions for each layer which is configured to be moving (speedFactor not null or zero) backgroundLayers.fastForEachWithIndex { index, layer -> @@ -34,18 +35,17 @@ class ParallaxSystem( val plane = parallaxDataContainer.config.parallaxPlane!! val parallaxMode = parallaxDataContainer.config.mode val texture = parallaxDataContainer.attachedLayersRear!!.defaultAnimation.firstFrame.layerData[index] - val globalOffset = plane.offset val attachTextureOffset = if (plane.attachedLayersRear!![index].attachBottomRight) texture.height else 0 if (parallaxMode == ParallaxConfig.Mode.HORIZONTAL_PLANE) { // Get correct speed factor for parallax plane lines from FloatArray - val lineIndex = texture.targetY - globalOffset + attachTextureOffset + val lineIndex = texture.targetY - offset + attachTextureOffset val speedFactor = plane.parallaxPlaneSpeedFactors[lineIndex] // Update position of parallax position parallaxPlane.attachedLayersRearPositions[index] = (speedFactor * velocityX * worldToPixelRatio) * deltaTime + position } else if (parallaxMode == ParallaxConfig.Mode.VERTICAL_PLANE) { // Get correct speed factor for parallax plane lines from FloatArray - val lineIndex = texture.targetX - globalOffset + attachTextureOffset + val lineIndex = texture.targetX - offset + attachTextureOffset val speedFactor = plane.parallaxPlaneSpeedFactors[lineIndex] // Update position of parallax position parallaxPlane.attachedLayersRearPositions[index] = (speedFactor * velocityY * worldToPixelRatio) * deltaTime + position @@ -55,7 +55,6 @@ class ParallaxSystem( parallaxPlane.linePositions.fastForEachWithIndex { index, position -> val plane = parallaxDataContainer.config.parallaxPlane!! val parallaxMode = parallaxDataContainer.config.mode - val offset = plane.offset if (parallaxMode == ParallaxConfig.Mode.HORIZONTAL_PLANE) { // Get correct speed factor for parallax plane lines from FloatArray val lineIndex = parallaxDataContainer.parallaxPlane!!.imageDatas[index].defaultAnimation.firstFrame.targetY - offset @@ -75,17 +74,16 @@ class ParallaxSystem( val plane = parallaxDataContainer.config.parallaxPlane!! val parallaxMode = parallaxDataContainer.config.mode val texture = parallaxDataContainer.attachedLayersFront!!.defaultAnimation.firstFrame.layerData[index] - val globalOffset = plane.offset val attachTextureOffset = if (plane.attachedLayersFront!![index].attachBottomRight) texture.height else 0 if (parallaxMode == ParallaxConfig.Mode.HORIZONTAL_PLANE) { // Get correct speed factor for parallax plane lines from FloatArray - val lineIndex = texture.targetY - globalOffset + attachTextureOffset + val lineIndex = texture.targetY - offset + attachTextureOffset val speedFactor = plane.parallaxPlaneSpeedFactors[lineIndex] // Update position of parallax position parallaxPlane.attachedLayersFrontPositions[index] = (speedFactor * velocityX * worldToPixelRatio) * deltaTime + position } else if (parallaxMode == ParallaxConfig.Mode.VERTICAL_PLANE) { // Get correct speed factor for parallax plane lines from FloatArray - val lineIndex = texture.targetX - globalOffset + attachTextureOffset + val lineIndex = texture.targetX - offset + attachTextureOffset val speedFactor = plane.parallaxPlaneSpeedFactors[lineIndex] // Update position of parallax position parallaxPlane.attachedLayersFrontPositions[index] = (speedFactor * velocityY * worldToPixelRatio) * deltaTime + position