diff --git a/src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt b/src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt index 6927cfa..0ffeca8 100644 --- a/src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt +++ b/src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt @@ -50,8 +50,8 @@ class EntityRegistry { components[id]?.remove(type) } - fun allWith(vararg types: KClass<*>): List = - components.entries + fun allWith(vararg types: KClass<*>): Sequence = + components.asSequence() .filter { (_, comps) -> types.all { it in comps } } .map { it.key } diff --git a/src/main/kotlin/dev/stillya/vpet/game/ecs/SpatialGrid.kt b/src/main/kotlin/dev/stillya/vpet/game/ecs/SpatialGrid.kt index f94c196..ac0e3ec 100644 --- a/src/main/kotlin/dev/stillya/vpet/game/ecs/SpatialGrid.kt +++ b/src/main/kotlin/dev/stillya/vpet/game/ecs/SpatialGrid.kt @@ -9,7 +9,7 @@ class SpatialGrid(private val cellSize: Int = 4) { private val cells = HashMap>() fun rebuild(registry: EntityRegistry) { - cells.clear() + for (set in cells.values) set.clear() for (id in registry.allWith(Transform::class, AABB::class)) { val t = registry.get(id) ?: continue val c = registry.get(id) ?: continue diff --git a/src/main/kotlin/dev/stillya/vpet/game/ecs/components/AnimationComponent.kt b/src/main/kotlin/dev/stillya/vpet/game/ecs/components/AnimationComponent.kt index 9c9e036..8ae06eb 100644 --- a/src/main/kotlin/dev/stillya/vpet/game/ecs/components/AnimationComponent.kt +++ b/src/main/kotlin/dev/stillya/vpet/game/ecs/components/AnimationComponent.kt @@ -1,7 +1,7 @@ package dev.stillya.vpet.game.ecs.components -data class AnimationComponent( +class AnimationComponent( val resourceId: String, - val currentFrame: Int = 0, - val elapsed: Float = 0f + var currentFrame: Int = 0, + var elapsed: Float = 0f ) diff --git a/src/main/kotlin/dev/stillya/vpet/game/ecs/systems/AnimationSystem.kt b/src/main/kotlin/dev/stillya/vpet/game/ecs/systems/AnimationSystem.kt index 4518dd9..0e5b916 100644 --- a/src/main/kotlin/dev/stillya/vpet/game/ecs/systems/AnimationSystem.kt +++ b/src/main/kotlin/dev/stillya/vpet/game/ecs/systems/AnimationSystem.kt @@ -7,28 +7,17 @@ import dev.stillya.vpet.game.resources.AnimationCache object AnimationSystem { fun updateAnimations(registry: EntityRegistry, dt: Float) { - val entities = registry.allWith(AnimationComponent::class) - - for (entityId in entities) { + for (entityId in registry.allWith(AnimationComponent::class)) { val component = registry.get(entityId) ?: continue val resource = AnimationCache.get(component.resourceId) ?: continue val frameCount = resource.animation.frameCount if (frameCount == 0) continue - val newElapsed = component.elapsed + dt - - if (newElapsed >= Physics.FRAME_ADVANCE_INTERVAL) { - val nextFrame = (component.currentFrame + 1) % frameCount - val updatedComponent = AnimationComponent( - resourceId = component.resourceId, - currentFrame = nextFrame, - elapsed = newElapsed - Physics.FRAME_ADVANCE_INTERVAL - ) - registry.add(entityId, updatedComponent) - } else { - val updatedComponent = component.copy(elapsed = newElapsed) - registry.add(entityId, updatedComponent) + component.elapsed += dt + if (component.elapsed >= Physics.FRAME_ADVANCE_INTERVAL) { + component.currentFrame = (component.currentFrame + 1) % frameCount + component.elapsed -= Physics.FRAME_ADVANCE_INTERVAL } } } diff --git a/src/main/kotlin/dev/stillya/vpet/graphics/DefaultIconRenderer.kt b/src/main/kotlin/dev/stillya/vpet/graphics/DefaultIconRenderer.kt index 17c77f6..91034c4 100644 --- a/src/main/kotlin/dev/stillya/vpet/graphics/DefaultIconRenderer.kt +++ b/src/main/kotlin/dev/stillya/vpet/graphics/DefaultIconRenderer.kt @@ -18,7 +18,6 @@ import javax.swing.Icon import javax.swing.ImageIcon import kotlin.math.roundToInt -// TODO: Add caching class DefaultIconRenderer(project: Project) : IconRenderer { private val settings get() = VPetSettings.getInstance() @@ -36,6 +35,7 @@ class DefaultIconRenderer(project: Project) : IconRenderer { private val verticalOffset: Int = -8 private var effect: Effect? = null private val epochManager = AnimationEpochManager() + private val renderCache = mutableMapOf>() companion object { private val log = logger() @@ -105,6 +105,9 @@ class DefaultIconRenderer(project: Project) : IconRenderer { } override fun setFlipped(flipped: Boolean) { + if (isFlipped != flipped) { + renderCache.clear() + } isFlipped = flipped } @@ -168,39 +171,44 @@ class DefaultIconRenderer(project: Project) : IconRenderer { } private fun doRender(animation: Animation): List { - return animation.extractFrames().map { frameImage -> - val scaledWidth = (frameImage.width * scaleValue).roundToInt() - val scaledHeight = (frameImage.height * scaleValue).roundToInt() - - val processedImage = if (isFlipped) { - val tx = AffineTransform.getScaleInstance(-1.0, 1.0) - tx.translate(-frameImage.width.toDouble(), 0.0) - val flippedImage = AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR) - .filter(frameImage, null) - flippedImage.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_DEFAULT) - } else { - frameImage.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_DEFAULT) - } + val key = "${animation.name}:$isFlipped" + return renderCache.getOrPut(key) { + animation.extractFrames().map { frameImage -> buildIcon(frameImage) } + } + } - object : ImageIcon(processedImage) { - override fun paintIcon( - c: java.awt.Component?, - g: java.awt.Graphics, - x: Int, - y: Int - ) { - if (settings.xmasModeEnabled) { - if (effect == null) { - effect = SnowflakeEffect(scaledWidth, scaledHeight) - } - val g2d = g.create() as java.awt.Graphics2D - g2d.translate(x, y) - val animState = currentAnimation?.state ?: AnimationState.IDLE - effect?.apply(g2d, animState) - g2d.dispose() + private fun buildIcon(frameImage: java.awt.image.BufferedImage): Icon { + val scaledWidth = (frameImage.width * scaleValue).roundToInt() + val scaledHeight = (frameImage.height * scaleValue).roundToInt() + + val processedImage = if (isFlipped) { + val tx = AffineTransform.getScaleInstance(-1.0, 1.0) + tx.translate(-frameImage.width.toDouble(), 0.0) + val flippedImage = AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR) + .filter(frameImage, null) + flippedImage.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_DEFAULT) + } else { + frameImage.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_DEFAULT) + } + + return object : ImageIcon(processedImage) { + override fun paintIcon( + c: java.awt.Component?, + g: java.awt.Graphics, + x: Int, + y: Int + ) { + if (settings.xmasModeEnabled) { + if (effect == null) { + effect = SnowflakeEffect(scaledWidth, scaledHeight) } - super.paintIcon(c, g, x, y + verticalOffset) + val g2d = g.create() as java.awt.Graphics2D + g2d.translate(x, y) + val animState = currentAnimation?.state ?: AnimationState.IDLE + effect?.apply(g2d, animState) + g2d.dispose() } + super.paintIcon(c, g, x, y + verticalOffset) } } } diff --git a/src/main/kotlin/dev/stillya/vpet/pet/PetAnimated.kt b/src/main/kotlin/dev/stillya/vpet/pet/PetAnimated.kt index 5cb5638..1e3830a 100644 --- a/src/main/kotlin/dev/stillya/vpet/pet/PetAnimated.kt +++ b/src/main/kotlin/dev/stillya/vpet/pet/PetAnimated.kt @@ -52,6 +52,9 @@ class PetAnimated( private var isObserving = AtomicBoolean(false) + private var cachedAnimationKey: Pair? = null + private var cachedAnimation: Animation? = null + @Volatile private var currentState: AnimationState = AnimationState.IDLE @@ -325,7 +328,9 @@ class PetAnimated( } private fun createAnimation(tag: String, loop: Int = 0): Animation? { - return runCatching { + val key = tag to loop + if (key == cachedAnimationKey) return cachedAnimation + val anim = runCatching { Animation( name = tag, loop = loop, @@ -334,6 +339,10 @@ class PetAnimated( state = AnimationState.IDLE ) }.getOrNull() + cachedAnimationKey = key + cachedAnimation = anim + + return anim } private fun processMovement(input: InputState, ctx: TickContext, dt: Float): Velocity { diff --git a/src/test/kotlin/dev/stillya/vpet/game/ecs/EntityRegistryTest.kt b/src/test/kotlin/dev/stillya/vpet/game/ecs/EntityRegistryTest.kt index bff4f61..2d73036 100644 --- a/src/test/kotlin/dev/stillya/vpet/game/ecs/EntityRegistryTest.kt +++ b/src/test/kotlin/dev/stillya/vpet/game/ecs/EntityRegistryTest.kt @@ -65,11 +65,11 @@ class EntityRegistryTest { val b = reg.create() reg.add(b, Transform()) - val both = reg.allWith(Transform::class, Velocity::class) + val both = reg.allWith(Transform::class, Velocity::class).toList() assertEquals(1, both.size) assertTrue(a in both) - val justTransform = reg.allWith(Transform::class) + val justTransform = reg.allWith(Transform::class).toList() assertEquals(2, justTransform.size) } diff --git a/src/test/kotlin/dev/stillya/vpet/game/ecs/systems/CoinSpawnerTest.kt b/src/test/kotlin/dev/stillya/vpet/game/ecs/systems/CoinSpawnerTest.kt index 49f7af1..c369f27 100644 --- a/src/test/kotlin/dev/stillya/vpet/game/ecs/systems/CoinSpawnerTest.kt +++ b/src/test/kotlin/dev/stillya/vpet/game/ecs/systems/CoinSpawnerTest.kt @@ -33,7 +33,7 @@ class CoinSpawnerTest { ) CoinSpawner.spawnCoins(registry, tileMap, 0..1, count = 3) - val coins = registry.allWith(AnimationComponent::class, Transform::class, Collectible::class, AABB::class) + val coins = registry.allWith(AnimationComponent::class, Transform::class, Collectible::class, AABB::class).toList() assertEquals(3, coins.size) coins.forEach { id -> @@ -60,7 +60,7 @@ class CoinSpawnerTest { ) CoinSpawner.spawnCoins(registry, tileMap, 0..1, count = 2) - val coins = registry.allWith(AnimationComponent::class) + val coins = registry.allWith(AnimationComponent::class).toList() assertEquals(2, coins.size) } @@ -74,7 +74,7 @@ class CoinSpawnerTest { ) CoinSpawner.spawnCoins(registry, tileMap, 0..1, count = 5) - val coins = registry.allWith(AnimationComponent::class) + val coins = registry.allWith(AnimationComponent::class).toList() assertEquals(0, coins.size) } @@ -89,7 +89,7 @@ class CoinSpawnerTest { ) CoinSpawner.spawnCoins(registry, tileMap, 0..2, count = 10) - val coins = registry.allWith(AnimationComponent::class, Transform::class) + val coins = registry.allWith(AnimationComponent::class, Transform::class).toList() assertTrue(coins.size > 0) coins.forEach { id -> @@ -110,7 +110,7 @@ class CoinSpawnerTest { ) CoinSpawner.spawnCoins(registry, tileMap, 0..2, count = 1) - val coins = registry.allWith(AnimationComponent::class, Transform::class) + val coins = registry.allWith(AnimationComponent::class, Transform::class).toList() assertEquals(1, coins.size) val transform = registry.get(coins.first())!! diff --git a/src/test/kotlin/dev/stillya/vpet/game/rendering/RenderSystemTest.kt b/src/test/kotlin/dev/stillya/vpet/game/rendering/RenderSystemTest.kt index 0ccaef0..c88d7b0 100644 --- a/src/test/kotlin/dev/stillya/vpet/game/rendering/RenderSystemTest.kt +++ b/src/test/kotlin/dev/stillya/vpet/game/rendering/RenderSystemTest.kt @@ -70,7 +70,7 @@ class RenderSystemTest { world.registry.add(coin2, Transform(x = 20f, y = 6f)) world.registry.add(coin2, AnimationComponent(resourceId = "coin_idle")) - val coins = world.registry.allWith(AnimationComponent::class, Transform::class) + val coins = world.registry.allWith(AnimationComponent::class, Transform::class).toList() assertEquals("Should have 2 coins", 2, coins.size) } @@ -102,7 +102,7 @@ class RenderSystemTest { val otherEntity = world.registry.create() world.registry.add(otherEntity, Transform(x = 25f, y = 7f)) - val coinsWithAnim = world.registry.allWith(AnimationComponent::class, Transform::class) + val coinsWithAnim = world.registry.allWith(AnimationComponent::class, Transform::class).toList() assertEquals("Should find only entities with both components", 2, coinsWithAnim.size) } }