
commonMain.com.lehaine.littlekt.AssetProvider.kt Maven / Gradle / Ivy
package com.lehaine.littlekt
import com.lehaine.littlekt.async.KtScope
import com.lehaine.littlekt.audio.AudioClip
import com.lehaine.littlekt.audio.AudioStream
import com.lehaine.littlekt.file.UnsupportedFileTypeException
import com.lehaine.littlekt.file.ldtk.LDtkMapLoader
import com.lehaine.littlekt.file.vfs.*
import com.lehaine.littlekt.graphics.Pixmap
import com.lehaine.littlekt.graphics.Texture
import com.lehaine.littlekt.graphics.g2d.TextureAtlas
import com.lehaine.littlekt.graphics.g2d.TextureSlice
import com.lehaine.littlekt.graphics.g2d.font.CharacterSets
import com.lehaine.littlekt.graphics.g2d.font.TtfFont
import com.lehaine.littlekt.graphics.g2d.tilemap.tiled.TiledMap
import com.lehaine.littlekt.graphics.gl.TexMagFilter
import com.lehaine.littlekt.graphics.gl.TexMinFilter
import com.lehaine.littlekt.util.fastForEach
import com.lehaine.littlekt.util.internal.lock
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
/**
* Provides helper functions to load and prepare assets without having to use `null` or `lateinit`.
*/
open class AssetProvider(val context: Context) {
private val assetsToPrepare = arrayListOf>()
private var totalAssetsLoading = atomic(0)
private var totalAssets = atomic(0)
private var totalAssetsFinished = atomic(0)
private val filesBeingChecked = mutableListOf()
private val _assets = mutableMapOf, MutableMap>>()
private val lock = Any()
val percentage: Float get() = if (totalAssets.value == 0) 1f else totalAssetsFinished.value / totalAssets.value.toFloat()
val loaders = createDefaultLoaders().toMutableMap()
val assets: Map, Map>> get() = _assets
var onFullyLoaded: (() -> Unit)? = null
/**
* Holds the current state of assets being prepared.
* @see prepare
*/
var prepared = false
protected set
/**
* Calls [update] to get the latest assets loaded to determine if is has been fully loaded.
*/
val fullyLoaded: Boolean
get() = totalAssetsLoading.value == 0 && prepared
private var job: Job? = null
/**
* Updates to check if all assets have been loaded, and if so, prepare them.
*/
fun update() {
if (totalAssetsLoading.value > 0) return
if (!prepared && job?.isActive != true) {
job = KtScope.launch {
assetsToPrepare.fastForEach {
it.prepare()
}
assetsToPrepare.clear()
prepared = true
onFullyLoaded?.invoke()
}
}
}
/**
* Loads an asset asynchronously.
* @param T concrete class of [Any] instance that should be loaded.
* @param file the file to load
* @param parameters any parameters that need setting when loading the asset
* @see BitmapFontAssetParameter
* @see LDtkGameAssetParameter
* @see TextureGameAssetParameter
* @see TtfFileAssetParameter
*/
fun load(
file: VfsFile,
clazz: KClass,
parameters: GameAssetParameters = EmptyGameAssetParameter(),
): GameAsset {
val sceneAsset = checkOrCreateNewSceneAsset(file, clazz)
context.vfs.launch {
loadVfsFile(sceneAsset, file, clazz, parameters)
}
return sceneAsset
}
/**
* Loads an asset in a suspending function.
* @param T concrete class of [Any] instance that should be loaded.
* @param file the file to load
* @param parameters any parameters that need setting when loading the asset
* @see BitmapFontAssetParameter
* @see LDtkGameAssetParameter
* @see TextureGameAssetParameter
* @see TtfFileAssetParameter
*/
suspend fun loadSuspending(
file: VfsFile,
clazz: KClass,
parameters: GameAssetParameters = EmptyGameAssetParameter(),
): GameAsset {
val sceneAsset = checkOrCreateNewSceneAsset(file, clazz)
loadVfsFile(sceneAsset, file, clazz, parameters)
return sceneAsset
}
@Suppress("UNCHECKED_CAST")
private fun checkOrCreateNewSceneAsset(
file: VfsFile,
clazz: KClass,
): GameAsset {
val sceneAsset = _assets[clazz]?.get(file)?.let {
return it as GameAsset
} ?: GameAsset(file)
if (filesBeingChecked.contains(file)) {
throw IllegalStateException("'${file.path}' has already been triggered to load but hasn't finished yet! Ensure `load()` hasn't been called twice for the same VfsFile")
}
prepared = false
filesBeingChecked += file
totalAssetsLoading.addAndGet(1)
totalAssets.addAndGet(1)
return sceneAsset
}
@Suppress("UNCHECKED_CAST")
private suspend fun loadVfsFile(
sceneAsset: GameAsset,
file: VfsFile,
clazz: KClass,
parameters: GameAssetParameters = EmptyGameAssetParameter(),
) {
val loader = loaders[clazz] ?: throw UnsupportedFileTypeException(file.path)
val result = loader.invoke(file, parameters) as T
sceneAsset.load(result)
lock(lock) {
_assets.getOrPut(clazz) { mutableMapOf() }.let {
it[file] = sceneAsset
}
filesBeingChecked -= file
}
totalAssetsFinished.addAndGet(1)
totalAssetsLoading.addAndGet(-1)
}
/**
* Loads an asset asynchronously.
* @param T concrete class of [Any] instance that should be loaded.
* @param file the file to load
* @param parameters any parameters that need setting when loading the asset
* @see BitmapFontAssetParameter
* @see LDtkGameAssetParameter
* @see TextureGameAssetParameter
* @see TtfFileAssetParameter
*/
inline fun load(
file: VfsFile,
parameters: GameAssetParameters = EmptyGameAssetParameter(),
) = load(file, T::class, parameters)
/**
* Loads an asset in a suspending function.
* @param T concrete class of [Any] instance that should be loaded.
* @param file the file to load
* @param parameters any parameters that need setting when loading the asset
* @see BitmapFontAssetParameter
* @see LDtkGameAssetParameter
* @see TextureGameAssetParameter
* @see TtfFileAssetParameter
*/
suspend inline fun loadSuspending(
file: VfsFile,
parameters: GameAssetParameters = EmptyGameAssetParameter(),
) = loadSuspending(file, T::class, parameters)
/**
* Prepares a value once assets have finished loading. This acts the same as [lazy] except this will
* invoke the [action] once loading is finished to ensure everything is initialized before the first frame.
* @param action the action to initialize this value
* @see load
*/
@OptIn(ExperimentalContracts::class)
fun prepare(action: suspend () -> T): PreparableGameAsset {
contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
return PreparableGameAsset(action).also { assetsToPrepare += it }
}
@Suppress("UNCHECKED_CAST")
fun get(clazz: KClass, vfsFile: VfsFile) = assets[clazz]?.get(vfsFile)?.content as T
inline fun get(
file: VfsFile,
) = get(T::class, file)
companion object {
fun createDefaultLoaders() = mapOf, suspend (VfsFile, GameAssetParameters) -> Any>(
Texture::class to { file, param ->
if (param is TextureGameAssetParameter) {
file.readTexture(param.minFilter, param.magFilter, param.useMipmaps)
} else {
file.readTexture()
}
},
Pixmap::class to { file, _ ->
file.readPixmap()
},
AudioClip::class to { file, _ ->
file.readAudioClip()
},
AudioStream::class to { file, _ ->
file.readAudioStream()
},
TextureAtlas::class to { file, _ ->
file.readAtlas()
},
TtfFont::class to { file, params ->
if (params is TtfFileAssetParameter) {
file.readTtfFont(params.chars)
} else {
file.readTtfFont()
}
},
com.lehaine.littlekt.graphics.g2d.font.BitmapFont::class to { file, params ->
if (params is BitmapFontAssetParameter) {
file.readBitmapFont(params.magFilter, params.mipmaps, params.preloadedTextures)
} else {
file.readBitmapFont()
}
},
LDtkMapLoader::class to { file, params ->
if (params is LDtkGameAssetParameter) {
file.readLDtkMapLoader(params.atlas, params.tilesetBorderThickness)
} else {
file.readLDtkMapLoader()
}
},
TiledMap::class to { file, _ ->
file.readTiledMap()
}
)
}
}
class GameAsset(val vfsFile: VfsFile) {
private var result: T? = null
private var isLoaded = false
val content get() = if (isLoaded) result!! else throw IllegalStateException("Asset not loaded yet! ${vfsFile.path}")
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = content
fun load(content: T) {
result = content
isLoaded = true
}
}
class PreparableGameAsset(val action: suspend () -> T) {
private var isPrepared = false
private var result: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (isPrepared) {
return result!!
} else {
throw IllegalStateException("Asset not prepared yet!")
}
}
suspend fun prepare() {
result = action.invoke()
isPrepared = true
}
}
interface GameAssetParameters
class EmptyGameAssetParameter : GameAssetParameters
class TextureGameAssetParameter(
val minFilter: TexMinFilter = TexMinFilter.NEAREST,
val magFilter: TexMagFilter = TexMagFilter.NEAREST,
val useMipmaps: Boolean = true,
) : GameAssetParameters
class LDtkGameAssetParameter(
val atlas: TextureAtlas? = null,
val tilesetBorderThickness: Int = 2,
) : GameAssetParameters
class TtfFileAssetParameter(
/**
* The chars to load a glyph for.
* @see CharacterSets
*/
val chars: String = CharacterSets.LATIN_ALL,
) : GameAssetParameters
class BitmapFontAssetParameter(
val magFilter: TexMagFilter = TexMagFilter.NEAREST,
/**
* Use mipmaps on the bitmap textures or not.
*/
val mipmaps: Boolean = true,
val preloadedTextures: List = listOf(),
) : GameAssetParameters
© 2015 - 2025 Weber Informatics LLC | Privacy Policy