From 64f825465de9fa30c4dfe2707067efdb96110db8 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Thu, 23 Oct 2025 21:59:37 -0700 Subject: Init --- .../coffee/liz/abstractionengine/game/Game.kt | 77 ++++++++++++++++++++++ .../liz/abstractionengine/game/ImageCache.kt | 23 +++++++ .../liz/abstractionengine/game/RenderSystem.kt | 70 ++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt create mode 100644 composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt create mode 100644 composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt (limited to 'composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game') diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt new file mode 100644 index 0000000..9490f83 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt @@ -0,0 +1,77 @@ +package coffee.liz.abstractionengine.game + +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import coffee.liz.ecs.World +import kotlinx.coroutines.isActive + +/** + * Manages the game loop for an ECS world. + * Syncs with Compose's frame rate (like requestAnimationFrame) and provides + * the world to child content for custom rendering/UI. + * + * @param world The ECS world containing entities and systems + * @param content Composable content that can use the world for rendering + */ +@Composable +fun Game( + world: World, + content: @Composable () -> Unit +) { + // Trigger recomposition when we need to redraw + var frameCount by remember { mutableStateOf(0) } + + // Game loop - syncs with Compose's frame rate like requestAnimationFrame + LaunchedEffect(world) { + var lastFrameTimeNanos = 0L + + while (isActive) { + // Wait for next frame (like requestAnimationFrame) + withFrameNanos { frameTimeNanos -> + val deltaTime = if (lastFrameTimeNanos != 0L) { + (frameTimeNanos - lastFrameTimeNanos) / 1_000_000_000f + } else { + 0f + } + lastFrameTimeNanos = frameTimeNanos + + // Update ECS world (runs all systems) + if (deltaTime > 0f) { + world.update(deltaTime) + } + + // Trigger recomposition to redraw + frameCount++ + } + } + } + + // Render content + content() +} + +/** + * A Canvas that renders sprites from an ECS world using a RenderSystem. + * + * @param world The ECS world containing entities to render + * @param renderSystem The system responsible for rendering sprites + * @param backgroundColor Background color for the canvas + * @param modifier Modifier for the Canvas + */ +@Composable +fun GameCanvas( + world: World, + renderSystem: RenderSystem, + backgroundColor: Color = Color.Transparent, + modifier: Modifier = Modifier +) { + Canvas(modifier = modifier) { + // Clear background + drawRect(backgroundColor) + + // Render all sprites + renderSystem.render(world, this) + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt new file mode 100644 index 0000000..0722535 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt @@ -0,0 +1,23 @@ +package coffee.liz.abstractionengine.game + +import androidx.compose.ui.graphics.ImageBitmap + +/** + * Simple cache for loaded images. + * In a real game, you'd want more sophisticated resource management. + */ +class ImageCache { + private val images = mutableMapOf() + + fun loadImage(path: String, image: ImageBitmap) { + images[path] = image + } + + fun getImage(path: String): ImageBitmap? { + return images[path] + } + + fun clear() { + images.clear() + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt new file mode 100644 index 0000000..1d22ae6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt @@ -0,0 +1,70 @@ +package coffee.liz.abstractionengine.game + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import coffee.liz.ecs.System +import coffee.liz.ecs.World +import coffee.liz.ecs.animation.AnimationSystem +import coffee.liz.ecs.animation.Animator +import coffee.liz.ecs.animation.Position +import coffee.liz.ecs.animation.SpriteSheet +import kotlin.reflect.KClass + +/** + * System that renders sprites to a DrawScope. + * + * This system queries all entities with Position, SpriteSheet, and Animator components, + * then draws their current animation frame to the provided DrawScope. + * + * Depends on AnimationSystem to ensure animations are updated before rendering. + */ +class RenderSystem( + private val imageCache: ImageCache +) : System { + + override val dependencies: Set> = setOf(AnimationSystem::class) + + /** + * The update method is required by the System interface, but rendering + * happens synchronously during Compose's draw phase via the render() method. + */ + override fun update(world: World, deltaTime: Float) { + // Rendering happens in render() method, not here + } + + /** + * Renders all entities with sprites to the given DrawScope. + * This should be called from a Compose Canvas during the draw phase. + */ + fun render(world: World, drawScope: DrawScope) { + // Query all entities that can be rendered + world.query(Position::class, SpriteSheet::class, Animator::class).forEach { entity -> + val position = entity.get(Position::class) ?: return@forEach + val spriteSheet = entity.get(SpriteSheet::class) ?: return@forEach + val animator = entity.get(Animator::class) ?: return@forEach + + // Get the current frame name from the animator + val frameName = animator.getCurrentFrameName() ?: return@forEach + + // Look up the frame rectangle in the sprite sheet + val frameRect = spriteSheet.frames[frameName] ?: return@forEach + + // Get the image from cache + val image = imageCache.getImage(spriteSheet.imagePath) ?: return@forEach + + // Draw the sprite + with(drawScope) { + drawImage( + image = image, + srcOffset = IntOffset(frameRect.x, frameRect.y), + srcSize = IntSize(frameRect.width, frameRect.height), + dstOffset = IntOffset(position.x.toInt(), position.y.toInt()), + dstSize = IntSize(frameRect.width, frameRect.height) + ) + } + } + } +} -- cgit v1.2.3-70-g09d2