Kotlin 272 - New Timer Ideas
GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo
Let’s think a bit about making a saucer timer and/or a ship timer, using our rudimentary component system. I think it’ll be easy and perhaps educational.
The Need
Let’s consider the saucer. I think it is the hardest of the two in some regards, so you could make a case for doing the ship first, but somehow I’m leaning toward the saucer.
From the simplest outlook, what the timer needs to do is, when the time designated has elapsed, it should activate the saucer if it is not active, deactivate it if it is active, and reset its time for next time. Somewhere in there we need to change the saucer direction, pick a starting Y coordinate, things like that. We have code now that does all that:
private var saucerSpeed = U.SaucerSpeed
var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
saucerGoneFor += deltaTime
if (saucerGoneFor > U.SaucerDelay) {
saucerGoneFor = 0.0
if (!Saucer.active) {
Saucer.active = true
Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
Saucer.velocity = Vector2(saucerSpeed, 0.0)
saucerSpeed *= -1.0
} else {
Saucer.active = false
}
}
}
The new saucer timer component will need to do much the same thing.
The existing Component and Timer look like this:
interface Component { val entity: SpaceObject }
data class Timer(override val entity: SpaceObject, val startTime: Double): Component {
var time = startTime
}
fun update(component: Component, deltaTime: Double) {
when (component) {
is Timer -> {
updateTimer(component, deltaTime)
}
}
}
private fun updateTimer(timer: Timer, deltaTime: Double) {
with(timer) {
if (!entity.active) return
time -= deltaTime
if (time <= 0.0) {
deactivate(entity)
time = timer.startTime
}
}
}
If the darn things were objects, of course they’d have their own update function. And maybe they should.
I think we could imagine a test for this. We could just create a SaucerTimer or whatever it might be, given a saucer as its entity, and then run it through its paces. Or pace, as it might be. Let’s do that. I think I’ll make a new test file for this.
@Test
fun `saucer timer`() {
val saucer = newSaucer()
assertThat(saucer.active).isEqualTo(false)
val timer = SaucerTimer(saucer)
updateSaucerTimer(timer, 0.5)
assertThat(saucer.active).isEqualTo(false)
}
This demands an object and a function:
data class SaucerTimer(override val entity: SpaceObject): Component {
var time = U.SaucerDelay
}
fun updateSaucerTimer(timer: SaucerTimer, deltaTime: Double) {
with(timer) {
time -= deltaTime
if (time <= 0.0) {
time = U.SaucerDelay
if (entity.active) {
entity.active = false
} else {
Saucer.active = true
Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
Saucer.velocity = Vector2(saucerSpeed, 0.0)
saucerSpeed *= -1.0
}
}
}
}
I just copy-pasted the logic over from the checkIfSaucerNeeded
. I think my test should run now. Whee, it does! Make it harder:
@Test
fun `saucer timer`() {
val saucer = newSaucer()
assertThat(saucer.active).isEqualTo(false)
val timer = SaucerTimer(saucer)
updateSaucerTimer(timer, 0.5)
assertThat(saucer.active).isEqualTo(false)
updateSaucerTimer(timer, U.SaucerDelay)
assertThat(saucer.active).describedAs("should start by now").isEqualTo(true)
}
Hmm, this didn’t pass. Why not? Ah, again, I used Saucer. Should refer to entity.
fun updateSaucerTimer(timer: SaucerTimer, deltaTime: Double) {
with(timer) {
time -= deltaTime
if (time <= 0.0) {
time = U.SaucerDelay
if (entity.active) {
entity.active = false
} else {
entity.active = true
entity.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
entity.velocity = Vector2(saucerSpeed, 0.0)
saucerSpeed *= -1.0
}
}
}
}
We’re green as Grogu now. Let’s commit: SaucerTimer initial test passes.
The test should be a bit more robust. Ideally we’ll check the position of the saucer, the position, the speed, and the sign of saucerSpeed
. Ideally. Let’s just do it.
@Test
fun `saucer timer`() {
val saucer = newSaucer()
saucer.position = Vector2(100.0, 100.0)
saucer.velocity = Vector2(0.0, 0.0)
val oldSpeed = saucerSpeed
assertThat(saucer.active).isEqualTo(false)
val timer = SaucerTimer(saucer)
updateSaucerTimer(timer, 0.5)
assertThat(saucer.active).isEqualTo(false)
updateSaucerTimer(timer, U.SaucerDelay)
assertThat(saucer.active).describedAs("should start by now").isEqualTo(true)
assertThat(saucer.x).describedAs("should start on y axis").isEqualTo(0.0)
assertThat(saucer.dx).describedAs("should start at oldSpeed").isEqualTo(oldSpeed)
assertThat(saucerSpeed).describedAs("should negate saucerSpeed").isEqualTo(-oldSpeed)
}
That all passes. Commit: SaucerTimer passes extensive testing regimen.
Now let’s create the Saucer with one of these beggars and skip the other updating thing. Oh and we’ll have to check for the update as well.
fun newSaucer(): SpaceObject = SpaceObject(
SpaceObjectType.SAUCER,
0.0,
0.0,
0.0,
0.0,
0.0,
false
).also { addComponent(it, SaucerTimer(it))}
fun update(component: Component, deltaTime: Double) {
when (component) {
is Timer -> {
updateTimer(component, deltaTime)
}
is SaucerTimer -> {
updateSaucerTimer(component, deltaTime)
}
}
}
Game works but that bug with the timer is back. Why? Because when we kill the saucer we reset the wrong value. This is a serious problem, masquerading as a simple bug.
Wait, are we even checking it?
fun checkSaucerVsMissile(saucer: SpaceObject, missile: SpaceObject) {
if (colliding(saucer, missile)) {
Score += U.SaucerScore
deactivate(saucer)
deactivate(missile)
}
}
I though we fixed that this morning? Oh, right, we did this, horribly:
fun deactivate(entity: SpaceObject) {
entity.active = false
for (component in entity.components) {
when (component) {
is Timer -> {
component.time = component.startTime
}
}
}
if (entity.type == SpaceObjectType.SHIP) shipGoneFor = 0.0
if (entity.type == SpaceObjectType.SAUCER) saucerGoneFor = 0.0
}
We’ll fix that this way for now:
fun deactivate(entity: SpaceObject) {
entity.active = false
for (component in entity.components) {
when (component) {
is Timer -> {
component.time = component.startTime
}
}
}
if (entity.type == SpaceObjectType.SHIP) shipGoneFor = 0.0
if (entity.type == SpaceObjectType.SAUCER) {
val timer = entity.components.find { it is SaucerTimer } as SaucerTimer
timer.time = U.SaucerDelay
}
}
Nasty. Should work tho. And it does. But we have some tests that should have failed and didn’t, though I don’t see how they could have given what they do:
All three of these saucer tests are now invalid:
fun `start saucer after seven seconds`() {
fun `saucer switches direction`() {
fun `saucer clock resets on death`() {
I’m not convinced that they have to be redone, but there should at least be one for shooting down the saucer. We’ll do that another day, this is more than enough for a Saturday afternoon.
Let’s sum up.
Summary
The lack of object methods is holding me back. There is probably a better way to approach things without them, but I am not seeing it. The SaucerTimer, on the other hand, is a decent improvement, removes a bit of procedural code from the Game cycle, moving it to a component, which is a thing we’d like to have happen.
We are, of course, searching the components list to find our components, and that’s surely not what we would do if we were concerned with performance, as game people generally are.
The deactivation of our objects is particularly problematical. It’s too easy to forget to make the necessary changes when a thing is taken out of the game.
And as for missiles … I think we’re … Oh! I just thought about this:
fun deactivate(entity: SpaceObject) {
entity.active = false
for (component in entity.components) {
when (component) {
is Timer -> {
component.time = component.startTime
}
}
}
if (entity.type == SpaceObjectType.SHIP) shipGoneFor = 0.0
if (entity.type == SpaceObjectType.SAUCER) {
val timer = entity.components.find { it is SaucerTimer } as SaucerTimer
timer.time = U.SaucerDelay
}
}
We can do that slightly more neatly, like this:
fun deactivate(entity: SpaceObject) {
entity.active = false
for (component in entity.components) {
when (component) {
is Timer -> {
component.time = component.startTime
}
is SaucerTimer -> {
component.time = U.SaucerDelay
}
}
}
if (entity.type == SpaceObjectType.SHIP) shipGoneFor = 0.0
}
Substantially nicer. Commit: SaucerTimer deactivation correctly handled. Tests need improvement / remodeling.
I’ve made new notes to improve those tests and improve components to be “faster”, which I’ll take to mean avoiding searching for them so much.
For now, I like the SaucerTimer and it went it pretty smoothly. See you next time!