Kotlin 277 - Another Time(r)
GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo
This morning I’ll just continue applying the new Timer, plus whatever catches my eye. At least that’s my plan here at the top of the article.
The missiles get a timer to turn them off, i.e. a “when active” timer:
fun newMissile(): SpaceObject {
return SpaceObject(SpaceObjectType.MISSILE, 0.0, 0.0, 0.0, 0.0, 0.0, false)
.also { spaceObject ->
val missileTimer = Timer(spaceObject, U.MissileTime, true) { timer-> deactivate(timer.entity)}
addComponent(spaceObject, missileTimer)
}
}
The ship, which I guess I’ll do next, needs a “when inactive” timer, and has a much more complicated action, where the missile just needs to deactivate.
I can’t think of a direct test for this, forgive me, but the existing tests will probably help—and will probably need changing. So let’s just do it.
There’s a function checkIfShipNeeded
that we’ll dispose of:
var shipGoneFor = 0.0
fun checkIfShipNeeded(deltaTime: Double) {
if ( ! Ship.active ) {
shipGoneFor += deltaTime
if (shipGoneFor > U.ShipDelay) {
dropScale = U.ShipDropInScale
Ship.position = Vector2(U.ScreenWidth/2.0, U.ScreenHeight/2.0)
Ship.velocity = Vector2(0.0,0.0)
Ship.angle = 0.0
Ship.active = true
shipGoneFor = 0.0
}
} else {
dropScale = max(dropScale - U.ShipDropInScale*deltaTime, 1.0)
}
}
But it does include the code we need when we restart the ship. Let’s extract that to a separate function that we can call.
var shipGoneFor = 0.0
fun checkIfShipNeeded(deltaTime: Double) {
if ( ! Ship.active ) {
shipGoneFor += deltaTime
if (shipGoneFor > U.ShipDelay) {
activateShip()
}
} else {
dropScale = max(dropScale - U.ShipDropInScale*deltaTime, 1.0)
}
}
fun activateShip() {
dropScale = U.ShipDropInScale
Ship.position = Vector2(U.ScreenWidth / 2.0, U.ScreenHeight / 2.0)
Ship.velocity = Vector2(0.0, 0.0)
Ship.angle = 0.0
Ship.active = true
shipGoneFor = 0.0
}
I’ll remove the call to the check function in the game. It turns out that three tests also call it. We’ll deal with those and they’ll probably suffice for testing the new scheme.
By the way …
With a small decently-tested object like the new Timer, a change like this is far easier to make than an ordinary change involving about the same amount of code. I feel quite comfortable that this will work with little or no difficulty. Except for the syntax of building the timer. We’ll see about that.
We need to change how the Ship is created, to give it the new Timer.
fun newShip(): SpaceObject = SpaceObject(SpaceObjectType.SHIP, 0.0, 0.0, 0.0, 0.0, 0.0, false)
We need an also
with a new Timer, similar to how we did the missile above. I think it goes like this:
fun newShip(): SpaceObject = SpaceObject(SpaceObjectType.SHIP, 0.0, 0.0, 0.0, 0.0, 0.0, false)
.also { spaceObject ->
val shipTimer = Timer(spaceObject, U.ShipDelay, false) { activateShip() }
addComponent(spaceObject, shipTimer)
}
I’ll test by running the game, becuase I know my tests aren’t checking this yet and I want to see if it’s time to fix them. Again, forgive me, I am weak.
Amusing! The ship does appear as advertised. But it stays large and does not shrink down with that dropping in animation that we love so much. So that’s odd. The activation code does set the dropScale. Where is it decremented? Ah! That’s done in the check’s else clause.
We’ll need to move that. Can it go in draw? Not easily: draw
doesn’t currently have deltaTime
, but it does have special handling for the ship:
fun draw(
spaceObject: SpaceObject,
drawer: Drawer,
) {
drawer.isolated {
val scale = 4.0 *spaceObject.scale
drawer.translate(spaceObject.x, spaceObject.y)
drawer.scale(scale, scale)
drawer.rotate(spaceObject.angle)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 1.0/scale
shipSpecialHandling(spaceObject, drawer)
drawer.lineStrip(spaceObject.type.points)
}
}
private fun shipSpecialHandling(spaceObject: SpaceObject, drawer: Drawer) {
if (spaceObject.type == SpaceObjectType.SHIP) {
drawer.scale(dropScale, dropScale)
if (Controls.accelerate && Random.nextInt(1, 3) == 1) {
drawer.lineStrip(shipFlare)
}
}
}
Seems pretty clear that we should pass deltaTime down here and put that dropScale code there. Another possibility, however, would be to patch it in right about here:
fun gameCycle(
spaceObjects: Array<SpaceObject>,
width: Int,
height: Int,
drawer: Drawer,
deltaTime: Double
) {
updateEverything(spaceObjects, deltaTime, width, height)
// RIGHT ABOUt HERE
drawEverything(spaceObjects, drawer)
checkCollisions()
drawScore(drawer)
checkIfAsteroidsNeeded(deltaTime)
}
Tacky, but does save passing a parameter all the way down. On the other hand, the updating really does belong in the special handling. Let’s do it right. Back down to the bottom and start having IDEA help us with change signature
.
private fun shipSpecialHandling(spaceObject: SpaceObject, drawer: Drawer, deltaTime: Double) {
if (spaceObject.type == SpaceObjectType.SHIP) {
dropScale = max(dropScale - U.ShipDropInScale*deltaTime, 1.0)
drawer.scale(dropScale, dropScale)
if (Controls.accelerate && Random.nextInt(1, 3) == 1) {
drawer.lineStrip(shipFlare)
}
}
}
I’ll spare you the others, they just add the parm.
The drop-in works as advertised:
Now about those tests. Find the senders of the check. They are all in one test for ship refresh:
@Test
fun `ship refresh`() {
createGame(U.MissileCount, U.AsteroidCount)
startGame(U.ScreenWidth, U.ScreenHeight)
Ship.active = false
checkIfShipNeeded(0.1)
checkIfShipNeeded(U.ShipDelay + 0.1)
assertThat(Ship.active).isEqualTo(true)
assertThat(dropScale).isEqualTo(U.ShipDropInScale)
checkIfShipNeeded(1.1)
assertThat(dropScale).isEqualTo(1.0)
}
I can certainly update the timers to check the creation, but the dropScale check is out of reach now, inside draw
. Let’s see about the other calls, though.
We don’t have a function to update all timers. Extract one:
private fun updateEverything(
spaceObjects: Array<SpaceObject>,
deltaTime: Double,
width: Int,
height: Int
) {
updateTimers(deltaTime)
for (spaceObject in spaceObjects) {
for (component in spaceObject.components) update(component, deltaTime)
if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
move(spaceObject, width, height, deltaTime)
}
}
fun updateTimers(deltaTime: Double) {
for (timer in TimerTable) {
updateTimer(timer, deltaTime)
}
}
Call it in the test.
@Test
fun `ship refresh`() {
createGame(U.MissileCount, U.AsteroidCount)
startGame(U.ScreenWidth, U.ScreenHeight)
Ship.active = false
updateTimers(0.1)
updateTimers(U.ShipDelay + 0.1)
assertThat(Ship.active).isEqualTo(true)
assertThat(dropScale).isEqualTo(U.ShipDropInScale)
updateTimers(1.1)
assertThat(dropScale).isEqualTo(1.0)
}
I expect to fail on the dropScale
check but to pass the other two. I say “expect”. “Hope” is more accurate.
Expected :1.0
Actual :3.0
Right. I know that works and don’t have a top-level way to check it. I’ll thank it and send it on its way, a little bit sad because I’ve lost a fragment of safety. Still, no one is likely to break the code. I hope …
I remove the check function, and that reminds me that shipGoneFor
is no longer needed, so I remove that.
I’m green, the game works. Commit: Ship renewal now done with Timer rather than explicit check.
Let’s reflect.
Reflection
This went about as smoothly as could be expected. The drop-in scaling bug was odd, though had I paid any attention at all I’d have noticed the update in the check function. Of course as soon as one runs the game one notices the immense ship that doesn’t shrink to normal size. And the test would have failed as well, had I run them, but I intentionally chose to watch the game work once, so that I’d feel right about even checking the tests.
Arguably a better way to have proceeded would have been to rewrite that test first and then do the change. I am not a perfect person and I was confident enough to go the other way, and lucky enough that it worked. Had I gone test first, it would have been maybe a bit more tedious and luck would not have been needed.
Don’t drive like my brother’s brother.
Saucer
We are left with the saucer. It will need two timers. This one I think I’d better do test first. Saucer has its own timer now:
data class SaucerTimer(override val entity: SpaceObject): Component {
var time = U.SaucerDelay
}
fun update(component: Component, deltaTime: Double) {
when (component) {
is SaucerTimer -> {
updateSaucerTimer(component, deltaTime)
}
}
}
fun updateSaucerTimer(timer: SaucerTimer, deltaTime: Double) {
with(timer) {
time -= deltaTime
if (time <= 0.0) {
time = U.SaucerDelay
if (entity.active) {
entity.active = false
} else {
activateSaucer(entity)
}
}
}
}
Are there tests for this? There is one:
@Test
fun `saucer timer activates after SaucerDelay`() {
val saucer = newSaucer()
val timer = SaucerTimer(saucer)
assertThat(saucer.active)
.describedAs("should start inactive").isEqualTo(false)
updateSaucerTimer(timer, 0.5)
assertThat(saucer.active)
.describedAs("should not activate immediately").isEqualTo(false)
updateSaucerTimer(timer, U.SaucerDelay)
assertThat(saucer.active)
.describedAs("should start by now").isEqualTo(true)
}
This is really just a test for the SaucerTimer object. No use to us, we’re not going to use it any more.
But wait!
Of course we could leave it as it stands. There’s no real reason to replace it. It works perfectly well. We’ve got another kind of Timer, which also works fine, and we’ve replaced some explicit code with the implicit timer code. We’ve proved the concept, and we’ve learned about all we’re going to learn about keeping components separate in the style of E-C-S (as I understand it).
I say we let this stand. There’s no value to replacing it and there is associated risk.
Any objections? No? OK, stet.
We’re done for the morning and, for now, with Timers and E-C-S learning. Let’s sum up.
Summary
I think it likely that we could reorganize the code a bit for better clarity, and we’ll surely look at that soon. The update
function above might want to be renamed updateIncludedComponents
or something, and we might want to combine that and updateTimers
into something with a suitable name.
One thing that I’ve realized is that I definitely do not have the reflexes of a real game programmer here in the 21st century. In particular, I don’t know what those folx do with regard to when they do or don’t pull out functions, whether they arrange their code in a special order, and so on, to optimize memory access. I just (think I) know that they care a lot about it. I’ve added a note to myself to find some E-C-S examples and browse them.
Looking forward for this program, I think we have a few interesting concerns. One of them is the safe emergence feature for the ship. Right now, the ship just emerges after the delay has elapsed, without checking to see if there are imminent collisions. As a courtesy, the real game, and our other versions, hold off on emergence until the center of the screen is fairly clear, until all missiles have expired, and until the saucer is gone.
I’m not sure how we’ll do that, which suggests that I should prioritize it and find out. Quite likely we can expand the action on the ship timer so that it doesn’t activate the ship and doesn’t reset the timer until the news is good. It might be that easy.
The primary point of this exercise was to compare the flat, no methods, pretend you’re writing in assembler style of this implementation with the more object-oriented ones already done. I can give you one key comparison right now:
I really prefer the object-oriented style. It helps me organize the code.
However, the code remains more compact than the O-O versions. It is getting harder to find things, but I think that alphabetizing the files might help with that. Maybe even break up the game file into creation and running or something.
E-C-S
As for the E-C-S style, I think I grasp a bit more of it, the notion that we’d put similar components together and process them all at once. Frankly, I think that from a readability, code-clarity viewpoint, E-C-S is inferior. I do think I like associating individual components with the entities … but processing the components in a batch is an optimization that reduces code simplicity. It may well be worth doing from a performance viewpoint, where that’s an issue, but it makes the code a bit harder to understand.
It’s certainly not fatally harder. But I wouldn’t do it unless I really needed the performance. I don’t see another advantage to it. If there is one … I hope someone advises me of it.
Today’s Bottom Line
The Timers work, we’ve learned a bit about E-C-S, our design has held up well, our tests have been easy to change as needed, and all’s well in outer space. A perfectly good Tuesday morning.
See you next time!