Skip to content

Commit

Permalink
Ease level loading and reloading
Browse files Browse the repository at this point in the history
When the data.json of a level is updated, the full level is updated and the script loaded again
  • Loading branch information
dwursteisen committed Nov 16, 2023
1 parent 0ec85f3 commit f3e2e47
Show file tree
Hide file tree
Showing 11 changed files with 67 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class GameEngine(
workEvents.forEach { resource ->
// The resource is loading
if (!resource.reload) {
logger.info("GAME_ENGINE") { "Loaded ${resource.name} ${resource.type}" }
logger.info("GAME_ENGINE") { "Loaded ${resource.name} ${resource.type} (version: ${resource.version})" }
when (resource.type) {
BOOT_GAMESCRIPT -> {
// Always put the boot script at the top of the stack
Expand Down Expand Up @@ -221,7 +221,7 @@ class GameEngine(
scripts[0]!!.resourcesLoaded()
}
} else {
logger.info("GAME_ENGINE") { "Reload ${resource.name} ${resource.type}" }
logger.info("GAME_ENGINE") { "Reload ${resource.name} ${resource.type} (version: ${resource.version})" }
// The resource already has been loaded.
when (resource.type) {
BOOT_GAMESCRIPT -> {
Expand Down Expand Up @@ -279,6 +279,8 @@ class GameEngine(

GAME_LEVEL -> {
levels[resource.index] = resource as GameLevel
// Force the reloading of the script as level init might occur in the _init block.
scripts[current]?.reload = true
}

GAME_SOUND -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class GfxLib(private val resourceAccess: GameResourceAccess) : TwoArgFunction()
copyFrom(frameBuffer.colorIndexBuffer) { index, _, _ -> index }
}
val sheet = SpriteSheet(
0,
arg.checkint(),
"frame_buffer",
ResourceType.GAME_SPRITESHEET,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ class KeysLib : TwoArgFunction() {
keys["space"] = LuaInteger.valueOf(Key.SPACE.ordinal)
keys["enter"] = LuaInteger.valueOf(Key.ENTER.ordinal)

arg2.set("keys", keys)
arg2.get("package").get("loaded").set("keys", keys)
arg2["keys"] = keys
arg2["package"]["loaded"]["keys"] = keys
return keys
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.github.mingdx.tiny.doc.TinyFunction
import com.github.mingdx.tiny.doc.TinyLib
import com.github.minigdx.tiny.Pixel
import com.github.minigdx.tiny.engine.GameResourceAccess
import com.github.minigdx.tiny.resources.GameLevel
import com.github.minigdx.tiny.resources.LdtkEntity
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
Expand All @@ -33,7 +34,8 @@ import kotlin.math.min
"WARNING: Projects need to be exported using " +
"https://ldtk.io/docs/game-dev/super-simple-export/['Super simple export']",
)
class MapLib(private val resourceAccess: GameResourceAccess, private val spriteSize: Pair<Pixel, Pixel>) : TwoArgFunction() {
class MapLib(private val resourceAccess: GameResourceAccess, private val spriteSize: Pair<Pixel, Pixel>) :
TwoArgFunction() {

private var currentLevel: Int = 0

Expand Down Expand Up @@ -186,15 +188,22 @@ entity.customFields -- access custom field of the entity

private val cachedEntities: MutableMap<Int, LuaValue> = mutableMapOf()

private var currentLevelVersion = currentLevel to -1

private val entities: LuaValue
get() {
return cachedEntities[currentLevel] ?: cacheMe(resourceAccess.level(currentLevel)?.entities)
// When the level is update, clear the cache.
val version = resourceAccess.level(currentLevel)?.version
if (currentLevel to version != currentLevelVersion) {
cachedEntities.clear()
}
return cachedEntities[currentLevel] ?: cacheMe(resourceAccess.level(currentLevel))
}

private fun cacheMe(entities: Map<String, List<LdtkEntity>>?): LuaValue {
private fun cacheMe(level: GameLevel?): LuaValue {
// Transform the list of entities into a table in Lua.
val toCache = LuaTable()
entities?.forEach { (key, v) ->
level?.entities?.forEach { (key, v) ->
val entitiesOfType = LuaTable()
v.forEach {
entitiesOfType[it.iid] = it.toLuaTable()
Expand All @@ -203,6 +212,7 @@ entity.customFields -- access custom field of the entity
}

cachedEntities[currentLevel] = toCache
currentLevelVersion = currentLevel to (level?.version ?: -1)
return toCache
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@ package com.github.minigdx.tiny.resources
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement

class GameLevel(
data class GameLevel(

override val version: Int,
override val index: Int,
override val type: ResourceType,
override val name: String,
val numberOfLayers: Int,
val ldktLevel: LdtkLevel,

) : GameResource {

override var reload: Boolean = false
val imageLayers: Array<LdKtImageLayer?> = Array(numberOfLayers) { null }
val intLayers: Array<LdKtIntLayer?> = Array(numberOfLayers) { null }
val entities = ldktLevel.entities

fun copy(): GameLevel {
val gameLevel = GameLevel(
version,
index,
type,
name,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.github.minigdx.tiny.resources

interface GameResource {
/**
* Version of the resource (ie: ~ number of time it has been reloaded)
* This version should be used only to know if the resources has been updated.
*/
val version: Int

/**
* Index of this game resource, by type.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.luaj.vm2.lib.StringLib
import org.luaj.vm2.lib.TableLib

class GameScript(
override val version: Int,
/**
* Index of game script
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,11 @@ import com.github.minigdx.tiny.resources.ResourceType.GAME_GAMESCRIPT
import com.github.minigdx.tiny.resources.ResourceType.GAME_LEVEL
import com.github.minigdx.tiny.resources.ResourceType.GAME_SPRITESHEET
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

class LdKtIntLayer(
Expand All @@ -47,7 +43,7 @@ class LdKtImageLayer(
var pixels: PixelArray = PixelArray(width, height),
)

@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@OptIn(ExperimentalCoroutinesApi::class)
class ResourceFactory(
private val vfs: VirtualFileSystem,
private val platform: Platform,
Expand All @@ -58,8 +54,9 @@ class ResourceFactory(
private val json = Json { ignoreUnknownKeys = true }

fun soundEffect(index: Int, name: String): Flow<Sound> {
var version = 0
return vfs.watch(platform.createSoundStream(name))
.map { soundData -> Sound(index, name, soundData) }
.map { soundData -> Sound(version++, index, name, soundData) }
.onEach {
logger.debug("RESOURCE_FACTORY") {
"Loading sound '$name'"
Expand All @@ -68,6 +65,7 @@ class ResourceFactory(
}

fun gameLevel(index: Int, name: String): Flow<GameLevel> {
var version = 0
return flowOf("$name/data.json")
.map { platform.createByteArrayStream(it) }
.flatMapMerge { filestream -> vfs.watch(filestream) }
Expand All @@ -78,8 +76,7 @@ class ResourceFactory(
logger.debug("RESOURCE_FACTORY") {
"Loading level " + level.uniqueIdentifer + " with layers " + level.layers.joinToString(", ")
}
}.flatMapMerge { level ->

}.map { level ->
val layers = listOf("$name/_composite.png") + level.layers.map { layer -> "$name/$layer" }
val pngLayers = layers
.mapIndexed { index, layer ->
Expand All @@ -91,29 +88,29 @@ class ResourceFactory(
width = level.width,
height = level.height,
)
}.asFlow()
.flatMapMerge { layer ->
vfs.watch(platform.createImageStream(layer.name)).map { imageData ->
convertToColorIndex(imageData.data, level.width, level.height)
}.map { texture ->
}.mapNotNull { layer ->
val stream = platform.createImageStream(layer.name)
if (stream.exists()) {
val imageData = stream.read()
val texture = convertToColorIndex(imageData.data, level.width, level.height)
layer.apply {
pixels = texture
}
} else {
null
}
}

val intLayers = layers
.map { layer -> layer.replace(".png", ".csv") }
.mapIndexed { index, layer ->
layer to index
}.asFlow()
.flatMapMerge { (layer, index) ->
vfs.watch(platform.createByteArrayStream(layer)).map { data ->
data.decodeToString()
.mapIndexedNotNull { index, layer ->
val stream = platform.createByteArrayStream(layer)
if (stream.exists()) {
val data = stream.read()
val lines = data.decodeToString()
.lines()
.map { l -> l.split(",").filter { it.isNotBlank() } }
.filterNot { it.isEmpty() }
}.map { lines ->
val l = LdKtIntLayer(
name = layer,
index = index,
Expand All @@ -129,21 +126,19 @@ class ResourceFactory(
}
}
l
} else {
null
}
}

flowOf(GameLevel(index, GAME_LEVEL, name, level.layers.size + 1, level))
.combine(pngLayers) { l, layer ->
l.apply {
imageLayers[layer.index] = layer
}
}.combine(intLayers) { l, layer ->
l.apply {
this.intLayers[layer.index] = layer
}
}.map {
it.copy()
GameLevel(version++, index, GAME_LEVEL, name, level.layers.size + 1, level).apply {
pngLayers.forEach { layer ->
imageLayers[layer.index] = layer
}
intLayers.forEach { layer ->
this.intLayers[layer.index] = layer
}
}.copy()
}
}

Expand All @@ -163,8 +158,9 @@ class ResourceFactory(
gameOptions: GameOptions,
resourceType: ResourceType,
): Flow<GameScript> {
var version = 0
return vfs.watch(platform.createByteArrayStream(name)).map { content ->
GameScript(index, name, gameOptions, inputHandler, resourceType).apply {
GameScript(version++, index, name, gameOptions, inputHandler, resourceType).apply {
this.content = content
}
}.onEach {
Expand All @@ -183,9 +179,10 @@ class ResourceFactory(
}

private fun spritesheet(index: Int, name: String, resourceType: ResourceType): Flow<SpriteSheet> {
var version = 0
return vfs.watch(platform.createImageStream(name)).map { imageData ->
val sheet = convertToColorIndex(imageData.data, imageData.width, imageData.height)
SpriteSheet(index, name, resourceType, sheet, imageData.width, imageData.height)
SpriteSheet(version++, index, name, resourceType, sheet, imageData.width, imageData.height)
}.onEach {
logger.debug("RESOURCE_FACTORY") {
"Loading spritesheet '$name' ($resourceType)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.minigdx.tiny.resources
import com.github.minigdx.tiny.platform.SoundData

data class Sound(
override val version: Int,
override val index: Int,
override val name: String,
val data: SoundData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.github.minigdx.tiny.Pixel
import com.github.minigdx.tiny.graphic.PixelArray

class SpriteSheet(
override val version: Int,
override val index: Int,
override val name: String,
override val type: ResourceType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class StdLibTest {

private val mockResources = object : GameResourceAccess {
override val bootSpritesheet: SpriteSheet = SpriteSheet(
0,
0,
"boot",
ResourceType.BOOT_SPRITESHEET,
Expand Down

0 comments on commit f3e2e47

Please sign in to comment.