Skip to content

Commit

Permalink
Fix scrolling of parallax layer background within game world (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
jobe-m authored Feb 17, 2025
1 parent 7c17a1f commit 8c8318b
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -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.
*
Expand All @@ -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
Expand All @@ -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<ParallaxLayerConfig>? = null,
val parallaxPlane: ParallaxPlaneConfig? = null,
Expand All @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CameraSystem>().parallaxHeight = parallaxLayerHeight
val parallaxLayerHeight: Float = imageHeight
this.system<CameraSystem>().parallaxHeight = parallaxLayerHeight - parallaxDataContainer.config.offset.toFloat()
this.system<CameraSystem>().parallaxOffset = parallaxDataContainer.config.offset.toFloat()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>("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) {

Expand Down Expand Up @@ -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")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 8c8318b

Please sign in to comment.