diff options
Diffstat (limited to 'composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation')
| -rw-r--r-- | composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt | 114 | ||||
| -rw-r--r-- | composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt | 77 |
2 files changed, 191 insertions, 0 deletions
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt new file mode 100644 index 0000000..4289eec --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt @@ -0,0 +1,114 @@ +package coffee.liz.ecs.animation + +import coffee.liz.ecs.Component +import kotlin.jvm.JvmInline + +/** + * Loop modes for animation playback. + */ +enum class LoopMode { + /** Play once and stop on last frame */ + ONCE, + /** Repeat from beginning */ + LOOP, + /** Play forward, then backward, repeat */ + PING_PONG +} + +/** + * Type-safe wrapper for animation names. + */ +@JvmInline +value class AnimationName(val value: String) + +/** + * Type-safe wrapper for frame names. + */ +@JvmInline +value class FrameName(val value: String) + +/** + * Represents a rectangle region in a sprite sheet. + */ +data class Rect( + val x: Int, + val y: Int, + val width: Int, + val height: Int +) + +/** + * Defines a single animation clip with frames and playback settings. + */ +data class AnimationClip( + val frameNames: List<FrameName>, + val frameDuration: Float, // seconds per frame + val loopMode: LoopMode = LoopMode.LOOP +) + +/** + * Component containing sprite sheet data - the image and frame definitions. + * This is immutable data that can be shared across entities. + */ +data class SpriteSheet( + val imagePath: String, + val frames: Map<FrameName, Rect> +) : Component + +/** + * Component for animation playback state. + * Contains animation clips and current playback state. + */ +data class Animator( + val clips: Map<AnimationName, AnimationClip>, + var currentClip: AnimationName, + var frameIndex: Int = 0, + var elapsed: Float = 0f, + var playing: Boolean = true, + var direction: Int = 1 // 1 for forward, -1 for backward (used in PING_PONG) +) : Component { + + /** + * Play a specific animation clip by name. + * Resets playback state when switching clips. + */ + fun play(clipName: AnimationName, restart: Boolean = true) { + if (currentClip != clipName || restart) { + currentClip = clipName + frameIndex = 0 + elapsed = 0f + direction = 1 + playing = true + } + } + + /** + * Pause the current animation. + */ + fun pause() { + playing = false + } + + /** + * Resume the current animation. + */ + fun resume() { + playing = true + } + + /** + * Get the current frame name being displayed. + */ + fun getCurrentFrameName(): FrameName? { + val clip = clips[currentClip] ?: return null + return clip.frameNames.getOrNull(frameIndex) + } +} + +/** + * Component for entity position in 2D space. + */ +data class Position( + var x: Float, + var y: Float +) : Component diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt new file mode 100644 index 0000000..7ae405e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt @@ -0,0 +1,77 @@ +package coffee.liz.ecs.animation + +import coffee.liz.ecs.System +import coffee.liz.ecs.World + +/** + * System that updates animation playback state for all entities with an Animator component. + * + * This system: + * - Updates elapsed time for playing animations + * - Advances frame indices based on frame duration + * - Handles loop modes (ONCE, LOOP, PING_PONG) + * - Stops animations when they complete (for ONCE mode) + */ +class AnimationSystem : System { + override fun update(world: World, deltaTime: Float) { + // Query all entities that have both Animator and SpriteSheet + world.query(Animator::class, SpriteSheet::class).forEach { entity -> + val animator = entity.get(Animator::class) ?: return@forEach + + // Skip if animation is not playing + if (!animator.playing) return@forEach + + // Get the current animation clip + val clip = animator.clips[animator.currentClip] ?: return@forEach + + // Accumulate elapsed time + animator.elapsed += deltaTime + + // Advance frames while we have enough elapsed time + while (animator.elapsed >= clip.frameDuration) { + animator.elapsed -= clip.frameDuration + advanceFrame(animator, clip) + } + } + } + + /** + * Advances the animation to the next frame based on loop mode. + */ + private fun advanceFrame(animator: Animator, clip: AnimationClip) { + animator.frameIndex += animator.direction + + when (clip.loopMode) { + LoopMode.ONCE -> { + // Play once and stop on last frame + if (animator.frameIndex >= clip.frameNames.size) { + animator.frameIndex = clip.frameNames.size - 1 + animator.playing = false + } + } + + LoopMode.LOOP -> { + // Loop back to beginning + if (animator.frameIndex >= clip.frameNames.size) { + animator.frameIndex = 0 + } else if (animator.frameIndex < 0) { + // Handle negative wrap (shouldn't happen in LOOP but be safe) + animator.frameIndex = clip.frameNames.size - 1 + } + } + + LoopMode.PING_PONG -> { + // Bounce back and forth + if (animator.frameIndex >= clip.frameNames.size) { + // Hit the end, reverse direction + animator.frameIndex = (clip.frameNames.size - 2).coerceAtLeast(0) + animator.direction = -1 + } else if (animator.frameIndex < 0) { + // Hit the beginning, reverse direction + animator.frameIndex = 1.coerceAtMost(clip.frameNames.size - 1) + animator.direction = 1 + } + } + } + } +} |
