diff --git a/CODEBASE_ANALYSIS.md b/CODEBASE_ANALYSIS.md new file mode 100644 index 0000000..fc2fbfa --- /dev/null +++ b/CODEBASE_ANALYSIS.md @@ -0,0 +1,148 @@ +# TinCan Codebase Analysis & Improvement Suggestions + +## Project Overview + +TinCan is a LibGDX-based mobile game written in Kotlin, targeting Android (primary) and Desktop. Players tap on tin cans to score points, with increasing difficulty via a spawner system. The codebase is compact (~29 Kotlin files) with a clean game-object architecture. + +--- + +## 1. Resource/Memory Management (High Priority) + +### Textures are never disposed +`GameObject.setTexture()` creates a new `Texture` on every call but never disposes the old one. In LibGDX, `Texture` is a native resource that must be explicitly disposed or it will leak GPU memory. + +**Affected files:** +- `GameObject.kt:67-68` — `setTexture(String)` creates `Texture` objects that are never freed +- `GameBackground.kt:10-11` — static `Texture` fields are never disposed +- `MenuButton.kt:27` — `whitePixelTexture` is created per-button instance, never disposed +- `Logo.kt`, `TinCanGame.kt` — similar issues + +**Suggestion:** Implement a centralized `AssetManager` or texture cache. Dispose textures in `TinCanGame.dispose()`. For `MenuButton`, share a single white-pixel texture across all instances rather than loading one per button. + +### Audio resources are never disposed +`Audio.kt` loads `Sound` and `Music` objects in `init()` but never calls `dispose()` on them. + +**Suggestion:** Add a `dispose()` method to `Audio` and call it from `TinCanGame.dispose()`. + +### SpriteBatch and BitmapFont disposal +`TinCanGame.dispose()` disposes `batch` but not `textFont`. The `BitmapFont` should also be disposed. + +--- + +## 2. Architecture & Design (Medium-High Priority) + +### Global mutable state via `object` singletons +`Director`, `Audio`, `GameBackground`, and `GameRandom` are all `object` singletons with mutable state. This creates tight coupling and makes the game difficult to test or reset cleanly. + +- `Director.gameObjects` is a public `CopyOnWriteArrayList` directly mutated from many classes (`Can.kt`, `Spawner.kt`, `SettingsManager.kt`, `GameObject.deleteSelf()`) +- `GameBackground.blindTimer` and `shakeTimer` are set from `Can.kt`, `Boom.kt`, and `GameOver.kt` + +**Suggestion:** Consider dependency injection or at minimum, reduce direct mutations. Have game objects return "events" or "commands" rather than directly modifying global state. For example, `Can.destroy()` could return a list of new objects to spawn instead of directly modifying `Director.gameObjects`. + +### `TinCanGame.storedData` and `textFont` are `lateinit` statics +These `companion object` fields create hidden global dependencies. Every UI class reaches into `TinCanGame.storedData` and `TinCanGame.textFont`. + +**Suggestion:** Pass these as constructor parameters or via a simple service locator to make dependencies explicit. + +### `Can` implements `Pool.Poolable` but is never pooled +`Can` implements `Pool.Poolable` and has a `reset()` method, but `Spawner` creates `Can()` with `new` every time. The pooling interface is unused. + +**Suggestion:** Either implement actual object pooling via `Pool` (would improve GC pressure on Android) or remove the `Poolable` interface to avoid confusion. + +--- + +## 3. Game Loop & Timing (Medium Priority) + +### Custom frame timing in `TinCanGame.update()` is fragile +The game uses `System.currentTimeMillis()` for manual frame pacing (lines 72-80), but LibGDX already provides `Gdx.graphics.deltaTime` for frame-independent updates. The current approach can skip or double-process frames unpredictably. + +**Suggestion:** Use `Gdx.graphics.deltaTime` for time-based movement instead of dividing by a constant `FPS`. This makes the game run smoothly on devices with varying frame rates. The `@Suppress("NOTHING_TO_INLINE")` annotation on the `update()` method is also unnecessary — the Kotlin compiler handles this fine without annotation. + +### Physics uses fixed FPS divisor instead of delta time +`GameObject.update()` divides velocities by `TinCanGame.FPS.toFloat()`. This means the game only runs correctly at exactly 60 FPS. + +**Suggestion:** Pass `deltaTime` to `update()` methods for frame-rate-independent physics. + +--- + +## 4. Potential Bugs (Medium Priority) + +### Concurrent modification in `Director.endGame()` +`Director.endGame()` (line 96-101) iterates `gameObjects` and calls `gameObjects.remove()` inside the loop. While `CopyOnWriteArrayList` makes this technically safe, it's inefficient (each `remove()` copies the entire array) and the pattern is error-prone. + +**Suggestion:** Collect items to remove, then remove them after the loop, or use `removeAll`. + +### `isTouched()` hit detection is oversized +`GameObject.isTouched()` checks a region of `2 * sprite.width + 2 * TOUCH_BUFFER` wide and `2 * sprite.height + 2 * TOUCH_BUFFER` tall (centered on the sprite). This means the touchable area extends one full sprite-width beyond the sprite's actual bounds in each direction, which is likely larger than intended. + +**Suggestion:** The check should probably use `sprite.width / 2` and `sprite.height / 2` instead of the full width/height, since it's measuring distance from center. + +### `GameBackground` shake only produces negative offsets +`drawShakingBackground()` uses `GameRandom.nextFloat(-8f, -1f)` for both X and Y, so the background only ever shakes down-left. A proper shake should go in all directions. + +**Suggestion:** Use `GameRandom.nextFloat(-8f, 8f)` for a realistic screen-shake effect. + +### Debug `println` statements left in production code +`TinCanGame.kt:48-49` has `println` calls logging touch coordinates on every tap. These will show up in logcat and waste I/O. + +**Suggestion:** Remove them or gate behind a debug flag. + +--- + +## 5. Code Quality & Kotlin Idioms (Low-Medium Priority) + +### `addSound()` can use `getOrPut` +```kotlin +// Current (Audio.kt:102-104) +val list = soundBank[tag] ?: mutableListOf() +list.add(sound) +soundBank[tag] = list + +// Improved +soundBank.getOrPut(tag) { mutableListOf() }.add(sound) +``` + +### `Debris` texture selection has an unreachable `else` branch +`Debris.kt:13` has `else -> throw RuntimeException("Bad random value")` after `nextInt(3)` which only returns 0, 1, or 2. The else is dead code. + +**Suggestion:** Use an array: `val textures = arrayOf("gib0.png", "gib1.png", "gib2.png")` and index directly. + +### `Can.updateImage()` magic numbers +The damage-to-image mapping uses a `when` block with magic numbers. Consider using an array or making `lethal` and image names data-driven. + +### `Logo` has dead copyright code +`Logo.kt` creates a label with empty string `""` and a comment "Disable for now." This is dead code that still allocates objects. + +### `MenuButton.touch()` has duplicated volume logic +The four volume branches in `MenuButton.kt:62-91` repeat the same pattern. This could be extracted into a helper. + +--- + +## 6. Build & Configuration (Low Priority) + +### Desktop module uses legacy LWJGL backend +The desktop project depends on `gdx-backend-lwjgl` (LWJGL 2). LibGDX recommends `gdx-backend-lwjgl3` for modern systems. + +### `DesktopStoredData` is a no-op stub +All methods return defaults and store nothing. If the desktop target is meant to be functional, it should use `Preferences` (LibGDX's cross-platform storage) or Java's `Preferences` API. + +### No unit tests +There are zero test files in the project. Core logic like `GameRandom`, `Director.increaseScore()`, and `Spawner` phase transitions are easily testable. + +**Suggestion:** Add a `core/test` source set and write unit tests for non-LibGDX logic. + +--- + +## 7. Summary of Priorities + +| Priority | Issue | Impact | +|----------|-------|--------| +| **High** | Texture/audio resource leaks | Memory leaks, potential OOM on Android | +| **High** | Debug println in production | I/O waste on every touch | +| **Medium** | No delta-time usage | Broken gameplay on non-60fps devices | +| **Medium** | Oversized touch hitbox | Gameplay feel / unintended taps | +| **Medium** | Shake only goes down-left | Visual bug | +| **Medium** | Unused Pool.Poolable on Can | Confusing/misleading code | +| **Low** | No tests | Maintainability | +| **Low** | Global mutable singletons | Testability, coupling | +| **Low** | Kotlin idiom improvements | Code cleanliness | diff --git a/core/src/io/chrislowe/tincan/Audio.kt b/core/src/io/chrislowe/tincan/Audio.kt index ac52d31..07b394e 100644 --- a/core/src/io/chrislowe/tincan/Audio.kt +++ b/core/src/io/chrislowe/tincan/Audio.kt @@ -70,8 +70,8 @@ object Audio { lastPlaying?.pause() } - fun updateAudio() { - crossFade -= CROSS_FADE_RATE + fun updateAudio(delta: Float) { + crossFade -= CROSS_FADE_RATE * delta * TinCanGame.FPS if (crossFade < 0f) crossFade = 0f val musicVol = TinCanGame.storedData.getMusicVolume() / 100f @@ -95,13 +95,27 @@ object Audio { currentlyPlaying?.volume = musicVol } + fun dispose() { + for (sounds in soundBank.values) { + for (sound in sounds) { + sound.dispose() + } + } + soundBank.clear() + + for (music in musicBank.values) { + music.dispose() + } + musicBank.clear() + + currentlyPlaying = null + lastPlaying = null + } + private fun addSound(tag: SoundTag, filename: String) { val file = Gdx.files.internal(filename) val sound = Gdx.audio.newSound(file) - - val list = soundBank[tag] ?: mutableListOf() - list.add(sound) - soundBank[tag] = list + soundBank.getOrPut(tag) { mutableListOf() }.add(sound) } private fun addMusic(tag: MusicTag, filename: String) { diff --git a/core/src/io/chrislowe/tincan/Director.kt b/core/src/io/chrislowe/tincan/Director.kt index 6deb971..f18bcf3 100644 --- a/core/src/io/chrislowe/tincan/Director.kt +++ b/core/src/io/chrislowe/tincan/Director.kt @@ -42,9 +42,9 @@ object Director { } } - fun updateGameObjects() { + fun updateGameObjects(delta: Float) { for (gameObject in gameObjects) { - gameObject.update() + gameObject.update(delta) } } @@ -80,7 +80,7 @@ object Director { hasHighScore = false val startCan = gameObjects.find { it is StartCan }!! - val can = Can() + val can = Can.pool.obtain() can.jumpToObject(startCan) gameObjects.clear() diff --git a/core/src/io/chrislowe/tincan/GameBackground.kt b/core/src/io/chrislowe/tincan/GameBackground.kt index 5e788a9..4362cec 100644 --- a/core/src/io/chrislowe/tincan/GameBackground.kt +++ b/core/src/io/chrislowe/tincan/GameBackground.kt @@ -1,22 +1,26 @@ package io.chrislowe.tincan -import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.SpriteBatch object GameBackground { private const val ZERO_OFFSET = 0f - private val whiteBackground = Texture(Gdx.files.internal("white.png")) - private val starBackground = Texture(Gdx.files.internal("stars.png")) + private lateinit var whiteBackground: Texture + private lateinit var starBackground: Texture - var blindTimer = 0 - var shakeTimer = 0 + fun init() { + whiteBackground = TextureCache.get("white.png") + starBackground = TextureCache.get("stars.png") + } + + var blindTimer = 0f + var shakeTimer = 0f - fun drawBackground(batch: SpriteBatch) { + fun drawBackground(batch: SpriteBatch, delta: Float) { when { - blindTimer > 0 -> {drawWhiteBackground(batch); blindTimer--} - shakeTimer > 0 -> {drawShakingBackground(batch); shakeTimer--} + blindTimer > 0f -> {drawWhiteBackground(batch); blindTimer -= delta} + shakeTimer > 0f -> {drawShakingBackground(batch); shakeTimer -= delta} else -> drawNormalBackground(batch) } } diff --git a/core/src/io/chrislowe/tincan/TextureCache.kt b/core/src/io/chrislowe/tincan/TextureCache.kt new file mode 100644 index 0000000..f4715d4 --- /dev/null +++ b/core/src/io/chrislowe/tincan/TextureCache.kt @@ -0,0 +1,21 @@ +package io.chrislowe.tincan + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Texture + +object TextureCache { + private val cache = mutableMapOf() + + fun get(filename: String): Texture { + return cache.getOrPut(filename) { + Texture(Gdx.files.internal(filename)) + } + } + + fun dispose() { + for (texture in cache.values) { + texture.dispose() + } + cache.clear() + } +} diff --git a/core/src/io/chrislowe/tincan/TinCanGame.kt b/core/src/io/chrislowe/tincan/TinCanGame.kt index 66bf44d..3902519 100644 --- a/core/src/io/chrislowe/tincan/TinCanGame.kt +++ b/core/src/io/chrislowe/tincan/TinCanGame.kt @@ -20,9 +20,6 @@ class TinCanGame(platformStoredData: StoredData) : ApplicationAdapter() { private lateinit var camera: OrthographicCamera private lateinit var batch: SpriteBatch - private val frameLifetime = 1000L / FPS - private var nextUpdate = 0L - init { storedData = platformStoredData } @@ -45,41 +42,34 @@ class TinCanGame(platformStoredData: StoredData) : ApplicationAdapter() { val touchX = screenX * scaleX val touchY = GAME_HEIGHT - (screenY * scaleY) - println("ScaleX: $scaleX, ScaleY: $scaleY") - println("TouchX: $touchX, TouchY: $touchY") - Director.handleTouchEvent(touchX, touchY) return true } } + GameBackground.init() Audio.init() } override fun render() { - update() + val delta = Gdx.graphics.deltaTime + + Director.updateGameObjects(delta) + Audio.updateAudio(delta) Gdx.gl.glClearColor(0f, 0f, 0f, 1f) Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) batch.begin() - GameBackground.drawBackground(batch) + GameBackground.drawBackground(batch, delta) Director.drawGameObjects(batch) batch.end() } - @Suppress("NOTHING_TO_INLINE") - private inline fun update() { - val startTime = System.currentTimeMillis() - if (startTime >= nextUpdate) { - Director.updateGameObjects() - Audio.updateAudio() - - nextUpdate = startTime + frameLifetime - } - } - override fun dispose() { batch.dispose() + textFont.dispose() + Audio.dispose() + TextureCache.dispose() } } diff --git a/core/src/io/chrislowe/tincan/objects/GameObject.kt b/core/src/io/chrislowe/tincan/objects/GameObject.kt index 6584484..c187c08 100644 --- a/core/src/io/chrislowe/tincan/objects/GameObject.kt +++ b/core/src/io/chrislowe/tincan/objects/GameObject.kt @@ -1,11 +1,10 @@ package io.chrislowe.tincan.objects -import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.Sprite import com.badlogic.gdx.graphics.g2d.SpriteBatch import io.chrislowe.tincan.Director -import io.chrislowe.tincan.TinCanGame +import io.chrislowe.tincan.TextureCache abstract class GameObject { companion object { @@ -20,21 +19,19 @@ abstract class GameObject { var gravity = 0f - var ticksUntilDestruction = -1 + var secondsUntilDestruction = -1f - open fun update() { - val fps = TinCanGame.FPS.toFloat() + open fun update(delta: Float) { + sprite.x += xVel * delta + sprite.y += yVel * delta + sprite.rotation += rotationVel * delta - sprite.x += xVel / fps - sprite.y += yVel / fps - sprite.rotation += rotationVel / fps + yVel += gravity * delta - yVel += gravity / fps + if (secondsUntilDestruction > 0f) { + secondsUntilDestruction -= delta - if (ticksUntilDestruction > 0) { - ticksUntilDestruction-- - - if (ticksUntilDestruction == 0) { + if (secondsUntilDestruction <= 0f) { deleteSelf() } } @@ -65,7 +62,7 @@ abstract class GameObject { } fun setTexture(filename: String) { - setTexture(Texture(Gdx.files.internal(filename))) + setTexture(TextureCache.get(filename)) } private fun setTexture(texture: Texture) { diff --git a/core/src/io/chrislowe/tincan/objects/game/Boom.kt b/core/src/io/chrislowe/tincan/objects/game/Boom.kt index 5b08335..c5d1b4f 100644 --- a/core/src/io/chrislowe/tincan/objects/game/Boom.kt +++ b/core/src/io/chrislowe/tincan/objects/game/Boom.kt @@ -1,13 +1,14 @@ package io.chrislowe.tincan.objects.game import io.chrislowe.tincan.GameBackground +import io.chrislowe.tincan.TinCanGame import io.chrislowe.tincan.objects.GameObject class Boom : GameObject() { init { setTexture("boom.png") - GameBackground.blindTimer = 1 - ticksUntilDestruction = 3 + GameBackground.blindTimer = 1f / TinCanGame.FPS + secondsUntilDestruction = 3f / TinCanGame.FPS } -} \ No newline at end of file +} diff --git a/core/src/io/chrislowe/tincan/objects/game/Can.kt b/core/src/io/chrislowe/tincan/objects/game/Can.kt index 0835e0b..b8dcbad 100644 --- a/core/src/io/chrislowe/tincan/objects/game/Can.kt +++ b/core/src/io/chrislowe/tincan/objects/game/Can.kt @@ -9,6 +9,12 @@ import io.chrislowe.tincan.objects.GameObject import io.chrislowe.tincan.objects.ui.PlusScore class Can : GameObject(), Pool.Poolable { + companion object { + val pool: Pool = object : Pool() { + override fun newObject(): Can = Can() + } + } + private val particleCount = 3 private var damage = 0 @@ -22,6 +28,9 @@ class Can : GameObject(), Pool.Poolable { sprite.rotation = 0f rotationVel = 0f gravity = -900f + xVel = 0f + yVel = 0f + secondsUntilDestruction = -1f damage = 0 @@ -58,8 +67,8 @@ class Can : GameObject(), Pool.Poolable { rotationVel = ((-rotationVel * 20) + xVel) / 10 } - override fun update() { - super.update() + override fun update(delta: Float) { + super.update(delta) if (rotationVel > 200f) rotationVel = 200f @@ -87,7 +96,7 @@ class Can : GameObject(), Pool.Poolable { } override fun touch(touchX: Float, touchY: Float) { - GameBackground.shakeTimer = 6 + GameBackground.shakeTimer = 6f / TinCanGame.FPS damage++ @@ -101,7 +110,7 @@ class Can : GameObject(), Pool.Poolable { fun destroy() { Audio.playSound(Audio.SoundTag.KILL) - GameBackground.blindTimer = 3 + GameBackground.blindTimer = 3f / TinCanGame.FPS val newObjects = ArrayList(particleCount + 2) (1..particleCount).forEach { _ -> @@ -120,7 +129,7 @@ class Can : GameObject(), Pool.Poolable { newObjects.add(plusScore) Director.gameObjects.addAll(newObjects) - Director.gameObjects.remove(this) + freeSelf() } private fun takeHit(hitX: Float, hitY: Float) { @@ -141,4 +150,9 @@ class Can : GameObject(), Pool.Poolable { punchUp() } -} \ No newline at end of file + + private fun freeSelf() { + Director.gameObjects.remove(this) + pool.free(this) + } +} diff --git a/core/src/io/chrislowe/tincan/objects/game/Debris.kt b/core/src/io/chrislowe/tincan/objects/game/Debris.kt index add706f..bb5a995 100644 --- a/core/src/io/chrislowe/tincan/objects/game/Debris.kt +++ b/core/src/io/chrislowe/tincan/objects/game/Debris.kt @@ -21,8 +21,8 @@ class Debris : GameObject() { gravity = -4000f } - override fun update() { - super.update() + override fun update(delta: Float) { + super.update(delta) if (sprite.y - sprite.height < 0) deleteSelf() } diff --git a/core/src/io/chrislowe/tincan/objects/game/Spark.kt b/core/src/io/chrislowe/tincan/objects/game/Spark.kt index 501a0ce..dce0ad9 100644 --- a/core/src/io/chrislowe/tincan/objects/game/Spark.kt +++ b/core/src/io/chrislowe/tincan/objects/game/Spark.kt @@ -1,6 +1,7 @@ package io.chrislowe.tincan.objects.game import io.chrislowe.tincan.GameRandom +import io.chrislowe.tincan.TinCanGame import io.chrislowe.tincan.objects.GameObject import io.chrislowe.tincan.plusOrMinus @@ -11,6 +12,6 @@ class Spark : GameObject() { xVel = 0f.plusOrMinus(2000f) yVel = 0f.plusOrMinus(2000f) - ticksUntilDestruction = 4 + secondsUntilDestruction = 4f / TinCanGame.FPS } } \ No newline at end of file diff --git a/core/src/io/chrislowe/tincan/objects/game/Spawner.kt b/core/src/io/chrislowe/tincan/objects/game/Spawner.kt index e3e6908..6bcd091 100644 --- a/core/src/io/chrislowe/tincan/objects/game/Spawner.kt +++ b/core/src/io/chrislowe/tincan/objects/game/Spawner.kt @@ -13,32 +13,35 @@ class Spawner : GameObject() { private val verticalRange = TinCanGame.GAME_HEIGHT / 6 private val horizontalRange = TinCanGame.GAME_WIDTH / 6 - private val upperBoundTicks = 180 - private val lowerBoundTicks = 60 + private val upperBoundSeconds = 180f / TinCanGame.FPS + private val lowerBoundSeconds = 60f / TinCanGame.FPS - private val upperBoundRange = 60 - private val lowerBoundRange = 30 + private val upperBoundRange = 60f / TinCanGame.FPS + private val lowerBoundRange = 30f / TinCanGame.FPS - private var minSpawnTicks = upperBoundTicks + private val decreasePerSpawnMin = 5f / TinCanGame.FPS + private val decreasePerSpawnRange = 3f / TinCanGame.FPS + + private var minSpawnSeconds = upperBoundSeconds private var spawnRange = upperBoundRange private var spawnPhase = SpawnPhase.SINGLE - private var spawnTicks = minSpawnTicks + spawnRange + private var spawnTimer = minSpawnSeconds + spawnRange private var spawnCount = 0 - override fun update() { - spawnTicks-- + override fun update(delta: Float) { + spawnTimer -= delta - if (spawnTicks == 0) { + if (spawnTimer <= 0f) { spawnCans() - if (minSpawnTicks > lowerBoundTicks) minSpawnTicks -= 5 - if (spawnRange > lowerBoundRange) spawnRange -= 3 + if (minSpawnSeconds > lowerBoundSeconds) minSpawnSeconds -= decreasePerSpawnMin + if (spawnRange > lowerBoundRange) spawnRange -= decreasePerSpawnRange if (spawnCount == SpawnPhase.DOUBLE.startsAt) changePhase(SpawnPhase.DOUBLE) if (spawnCount == SpawnPhase.TRIPLE.startsAt) changePhase(SpawnPhase.TRIPLE) - spawnTicks = minSpawnTicks + GameRandom.nextInt(spawnRange) + spawnTimer = minSpawnSeconds + GameRandom.nextFloat(0f, spawnRange) } } @@ -55,12 +58,12 @@ class Spawner : GameObject() { private fun changePhase(newPhase: SpawnPhase) { spawnPhase = newPhase - minSpawnTicks = upperBoundTicks + minSpawnSeconds = upperBoundSeconds spawnRange = upperBoundRange } private fun spawnRightCan() { - val can = Can() + val can = Can.pool.obtain() can.sprite.x = TinCanGame.GAME_WIDTH can.sprite.y = (TinCanGame.GAME_HEIGHT / 2).plusOrMinus(verticalRange) can.xVel = (-800f).plusOrMinus(150f) @@ -69,7 +72,7 @@ class Spawner : GameObject() { } private fun spawnLeftCan() { - val can = Can() + val can = Can.pool.obtain() can.sprite.x = -can.sprite.width can.sprite.y = (TinCanGame.GAME_HEIGHT / 2).plusOrMinus(verticalRange) can.xVel = 800f.plusOrMinus(150f) @@ -78,11 +81,11 @@ class Spawner : GameObject() { } private fun spawnUpperCan() { - val can = Can() + val can = Can.pool.obtain() can.sprite.x = (TinCanGame.GAME_WIDTH / 2).plusOrMinus(horizontalRange) can.sprite.y = TinCanGame.GAME_HEIGHT + can.sprite.height can.xVel = 0f can.yVel = 0f Director.gameObjects.add(can) } -} \ No newline at end of file +} diff --git a/core/src/io/chrislowe/tincan/objects/ui/EndMessage.kt b/core/src/io/chrislowe/tincan/objects/ui/EndMessage.kt index 26ea468..50451c1 100644 --- a/core/src/io/chrislowe/tincan/objects/ui/EndMessage.kt +++ b/core/src/io/chrislowe/tincan/objects/ui/EndMessage.kt @@ -7,7 +7,7 @@ import kotlin.math.sin class EndMessage(hasHighScore: Boolean) : GameObject() { private val bobAmount = 64 - private var ticksAlive = 0 + private var timeAlive = 0f init { val imageName = if (hasHighScore) "congrats.png" else "tryagain.png" @@ -18,15 +18,14 @@ class EndMessage(hasHighScore: Boolean) : GameObject() { sprite.y = 2f * (TinCanGame.GAME_HEIGHT - sprite.height) / 3f } - override fun update() { - super.update() + override fun update(delta: Float) { + super.update(delta) - ticksAlive++ + timeAlive += delta val screenMiddle = TinCanGame.GAME_HEIGHT / 3f - val fps = TinCanGame.FPS.toFloat() - val sinePosition = sin(Math.PI * (ticksAlive / fps)).toFloat() + val sinePosition = sin(Math.PI * timeAlive.toDouble()).toFloat() sprite.y = screenMiddle + sinePosition * bobAmount } -} \ No newline at end of file +} diff --git a/core/src/io/chrislowe/tincan/objects/ui/GameOver.kt b/core/src/io/chrislowe/tincan/objects/ui/GameOver.kt index 5a30de3..47bfea1 100644 --- a/core/src/io/chrislowe/tincan/objects/ui/GameOver.kt +++ b/core/src/io/chrislowe/tincan/objects/ui/GameOver.kt @@ -8,8 +8,9 @@ import io.chrislowe.tincan.plusOrMinus class GameOver : GameObject() { private val shakeScale = 16 + private val shakeDuration = 12f / TinCanGame.FPS - private var selfShakeTimer = 12 + private var selfShakeTimer = shakeDuration init { GameBackground.shakeTimer = selfShakeTimer @@ -20,19 +21,19 @@ class GameOver : GameObject() { selfCenter() } - override fun update() { - if (selfShakeTimer > 0) { - selfShakeTimer-- + override fun update(delta: Float) { + if (selfShakeTimer > 0f) { + selfShakeTimer -= delta selfCenter() - if (selfShakeTimer != 0) { + if (selfShakeTimer > 0f) { sprite.x += 0.plusOrMinus(shakeScale) sprite.y += 0.plusOrMinus(shakeScale) } } } - override fun isTouched(touchX: Float, touchY: Float) = (selfShakeTimer == 0) + override fun isTouched(touchX: Float, touchY: Float) = (selfShakeTimer <= 0f) override fun touch(touchX: Float, touchY: Float) = Director.changeGameState(Director.GameState.MENU) @@ -41,4 +42,4 @@ class GameOver : GameObject() { sprite.x = TinCanGame.GAME_WIDTH / 2f - sprite.width / 2f sprite.y = TinCanGame.GAME_HEIGHT * (2 / 3f) } -} \ No newline at end of file +} diff --git a/core/src/io/chrislowe/tincan/objects/ui/MenuButton.kt b/core/src/io/chrislowe/tincan/objects/ui/MenuButton.kt index 5cdc304..e525f3c 100644 --- a/core/src/io/chrislowe/tincan/objects/ui/MenuButton.kt +++ b/core/src/io/chrislowe/tincan/objects/ui/MenuButton.kt @@ -1,12 +1,11 @@ package io.chrislowe.tincan.objects.ui -import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.utils.Align import io.chrislowe.tincan.Audio +import io.chrislowe.tincan.TextureCache import io.chrislowe.tincan.TinCanGame import io.chrislowe.tincan.objects.GameObject import io.chrislowe.tincan.ui.SettingsManager @@ -24,7 +23,7 @@ class MenuButton( buttonText: String ) : GameObject() { - private val whitePixelTexture = Texture(Gdx.files.internal("white.png")) + private val whitePixelTexture = TextureCache.get("white.png") private val blackTextStyle = Label.LabelStyle(TinCanGame.textFont, Color.BLACK) private val label: Label private var buttonWidth = 80f diff --git a/core/src/io/chrislowe/tincan/objects/ui/PlusScore.kt b/core/src/io/chrislowe/tincan/objects/ui/PlusScore.kt index 5e43f79..a7d0ff2 100644 --- a/core/src/io/chrislowe/tincan/objects/ui/PlusScore.kt +++ b/core/src/io/chrislowe/tincan/objects/ui/PlusScore.kt @@ -22,18 +22,18 @@ class PlusScore(bonus: Int) : GameObject() { label.setSize(TinCanGame.GAME_WIDTH, label.height) label.setPosition(xOffset, yOffset) - ticksUntilDestruction = 30 + secondsUntilDestruction = 30f / TinCanGame.FPS Director.increaseScore(bonus) } - override fun update() { - super.update() + override fun update(delta: Float) { + super.update(delta) - label.y += 50f / TinCanGame.FPS + label.y += 50f * delta } override fun draw(batch: SpriteBatch) { label.draw(batch, 1f) } -} \ No newline at end of file +} diff --git a/core/src/io/chrislowe/tincan/objects/ui/StartCan.kt b/core/src/io/chrislowe/tincan/objects/ui/StartCan.kt index d80476c..898e70f 100644 --- a/core/src/io/chrislowe/tincan/objects/ui/StartCan.kt +++ b/core/src/io/chrislowe/tincan/objects/ui/StartCan.kt @@ -9,7 +9,7 @@ import kotlin.math.sin class StartCan : GameObject() { private val bobAmount = 64 - private var ticksAlive = 0 + private var timeAlive = 0f init { setTexture("can0.png") @@ -18,14 +18,13 @@ class StartCan : GameObject() { sprite.y = TinCanGame.GAME_HEIGHT / 2f } - override fun update() { - super.update() + override fun update(delta: Float) { + super.update(delta) - ticksAlive++ + timeAlive += delta val screenMiddle = TinCanGame.GAME_WIDTH / 2f - sprite.width / 2f - val fps = TinCanGame.FPS.toFloat() - val sinePosition = sin(Math.PI * (ticksAlive / fps)).toFloat() + val sinePosition = sin(Math.PI * timeAlive.toDouble()).toFloat() sprite.x = screenMiddle + sinePosition * bobAmount } @@ -34,4 +33,4 @@ class StartCan : GameObject() { Audio.playSound(Audio.SoundTag.HIT) Director.changeGameState(Director.GameState.PLAYING) } -} \ No newline at end of file +} diff --git a/core/src/io/chrislowe/tincan/objects/ui/TutorialIcon.kt b/core/src/io/chrislowe/tincan/objects/ui/TutorialIcon.kt index 61f8ee2..c1de3c5 100644 --- a/core/src/io/chrislowe/tincan/objects/ui/TutorialIcon.kt +++ b/core/src/io/chrislowe/tincan/objects/ui/TutorialIcon.kt @@ -10,8 +10,8 @@ class TutorialIcon(private val parent: GameObject) : GameObject() { else setTexture("taphere.png") } - override fun update() { - super.update() + override fun update(delta: Float) { + super.update(delta) jumpToObject(parent) sprite.x += 70