Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class EntityRegistry {
components[id]?.remove(type)
}

fun allWith(vararg types: KClass<*>): List<EntityID> =
components.entries
fun allWith(vararg types: KClass<*>): Sequence<EntityID> =
components.asSequence()
.filter { (_, comps) -> types.all { it in comps } }
.map { it.key }

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/dev/stillya/vpet/game/ecs/SpatialGrid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SpatialGrid(private val cellSize: Int = 4) {
private val cells = HashMap<Long, MutableSet<EntityID>>()

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<Transform>(id) ?: continue
val c = registry.get<AABB>(id) ?: continue
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnimationComponent>(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
}
}
}
Expand Down
70 changes: 39 additions & 31 deletions src/main/kotlin/dev/stillya/vpet/graphics/DefaultIconRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<String, List<Icon>>()

companion object {
private val log = logger<DefaultIconRenderer>()
Expand Down Expand Up @@ -105,6 +105,9 @@ class DefaultIconRenderer(project: Project) : IconRenderer {
}

override fun setFlipped(flipped: Boolean) {
if (isFlipped != flipped) {
renderCache.clear()
}
isFlipped = flipped
}

Expand Down Expand Up @@ -168,39 +171,44 @@ class DefaultIconRenderer(project: Project) : IconRenderer {
}

private fun doRender(animation: Animation): List<Icon> {
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)
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/main/kotlin/dev/stillya/vpet/pet/PetAnimated.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class PetAnimated(

private var isObserving = AtomicBoolean(false)

private var cachedAnimationKey: Pair<String, Int>? = null
private var cachedAnimation: Animation? = null

@Volatile
private var currentState: AnimationState = AnimationState.IDLE

Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand All @@ -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)
}

Expand All @@ -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)
}

Expand All @@ -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 ->
Expand All @@ -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<Transform>(coins.first())!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
}
Loading