Kotlin 186: What's On The List?
Going to try for a light morning, as I have an appointment. Let’s check the checklist and pick some small but beneficial changes
Here’s the list of things to do. I’ve saved it in an editor tab. Better than Jira1.
Priority = H/M/L; Done = √; Internal Improvement = (i)
- H Ship should slow à la friction.
- H Sound???
- H Small saucer (1000 points) appears after 30,000 points.
- H Small saucer shot accuracy improves. (Needs research.)
- M Add a sun with gravity?
- √ (i, more to do) Improve generality of graphics, stroke width vs scale etc.
- √ (i, more to do) Eliminate magic numbers, moving to Universe.
- M Ability to change some settings from keyboard (cheat codes)
- M (i) Timing code could be improved to be more consistent. OneShot?
- L Some write-ups say that small asteroids are faster. (Needs research)
- L Saucer zig-zagging could be a bit more random.
-
L Allow for non-square windows?
- √ Ship exhaust flare
- √ Ship turning seems a bit slow. 180->200. Accuracy seems fine.
- √ Ship acceleration seems sluggish. (1000 -> 1200)
- √ Hyperspace return away from edges for visibility.
- √ Check general scale of ship etc against original.
- √ (i, more to do) Improve generality of graphics, stroke width vs scale etc.
- √ (i, more to do) Eliminate magic numbers, moving to Universe.
I moved done things down below the things that still need to be done. Duplicated a couple of entries. All in moments. Might change the format again tomorrow. Take that, Jira2.
I should mention that last night, during the Zoom session, I tried an idea. I don’t like the way that after the ship is killed, the Saucer still flies around firing missiles. It makes clutter, kills asteroids, and then the game waits for the missiles to peter out, before it can rez a “new” ship. I have code in there now that kills the saucer as soon as the ship is gone:
class Saucer : ISpaceObject, InteractingSpaceObject, Collider {
...
var sawShip = false
var shipFuturePosition = Point.ZERO
override val subscriptions = Subscriptions(
draw = this::draw,
beforeInteractions = { sawShip = false },
interactWithAsteroid = { asteroid, trans -> checkCollision(asteroid, trans) },
interactWithShip = { ship, trans ->
sawShip = true
shipFuturePosition = ship.position + ship.velocity*1.5
checkCollision(ship, trans) },
interactWithMissile = { missile, trans -> checkCollision(missile, trans) },
finalize = this::finalize
)
override fun update(deltaTime: Double, trans: Transaction) {
if ( sawShip ) {
elapsedTime += deltaTime
if (elapsedTime > U.SAUCER_LIFETIME) trans.remove(this)
timeSinceSaucerSeen += deltaTime
if (timeSinceSaucerSeen > 1.5) zigZag()
timeSinceLastMissileFired += deltaTime
if (timeSinceLastMissileFired > 0.5 ) fire(trans)
position = (position + velocity * deltaTime).cap()
} else {
trans.remove(this)
}
}
We assume there’s no ship. If we see it, we record that we saw it. If, in the next update, we haven’t seen the ship, we remove the saucer.
I don’t like that,, because at the same instant as the ship dies, the saucer vanishes. It doesn’t look right. Let’s change it so that the Saucer stops firing, but lives out its time.
Remove the if stuff in update
:
override fun update(deltaTime: Double, trans: Transaction) {
elapsedTime += deltaTime
if (elapsedTime > U.SAUCER_LIFETIME) trans.remove(this)
timeSinceSaucerSeen += deltaTime
if (timeSinceSaucerSeen > 1.5) zigZag()
timeSinceLastMissileFired += deltaTime
if (timeSinceLastMissileFired > 0.5) fire(trans)
position = (position + velocity * deltaTime).cap()
}
And don’t fire if there’s no ship:
private fun fire(trans: Transaction) {
if ( sawShip ) {
if (Random.nextInt(4) == 0 ) fireTargeted(trans)
else fireRandom(trans)
}
}
That should do it. Should we have written a test for that? Yes, we should have.
@Test
fun `saucer will fire if ship present`() {
val saucer = Saucer()
saucer.sawShip = true
val trans = Transaction()
saucer.fire(trans)
assertThat(trans.adds.size).isEqualTo(1)
}
@Test
fun `saucer will not fire if ship not present`() {
val saucer = Saucer()
// saucer.sawShip = true
val trans = Transaction()
saucer.fire(trans)
assertThat(trans.adds.size).isEqualTo(0)
}
I commented out the sawShip
line in the second test to make it more clear just what is going on. Would it be better to set it false? I guess it would. Let’s do that.
@Test
fun `saucer will not fire if ship not present`() {
val saucer = Saucer()
saucer.sawShip = false
val trans = Transaction()
saucer.fire(trans)
assertThat(trans.adds.size).isEqualTo(0)
}
Should we have a round-trip test that works through all the interactions? This morning, I’m voting no on that. It would be easy enough, but somehow I feel it’s not going to provide me value.
Oh, hell. It might provide value later, if I play with the Saucer, and we know that saucer is going to see some modifications, so let’s do the scenario test. Grr. I hate it when I guilt myself into doing the right thing.
@Test
fun `saucer knows if ship present`() {
val saucer = Saucer()
saucer.subscriptions.beforeInteractions()
assertThat(saucer.sawShip).isEqualTo(false)
saucer.subscriptions.interactWithShip(Ship(U.CENTER_OF_UNIVERSE), Transaction())
assertThat(saucer.sawShip).isEqualTo(true)
}
That was easy enough, and could conceivably pay off. I often find that when I overcome my resistance and write a test, it’s far easier than my resistance level would suggest. Perhaps I am not a good person. Anyway, useful test, since we are fiddling in the area of ship-saucer interaction.
This does what we wanted. Commit: Saucer stops firing when ship is absent. Avoids clutter.
I’ll make a checked-off item for the list, so that I can be sure to get credit for the improvement.
I only have a few minutes before my appointment, so let’s sum up. I’ll start a new article if I want to do more work later.
Summary
Just a tiny change, but it goes in very smoothly. Look for the ship, note that you’ve seen it, if you haven’t seen it, don’t fire missiles. Everything in its place, no twisting or bending awkwardly to get it done. That’s a good sign. If the code resists reasonable changes, that’s a strong hint that there’s something about the design that could be improved. When things go smoothly, the design is supporting that kind of change. So, a good sign.
The elephant in the room is that I am clearly avoiding working on sound. My reasons are not very good ones. I know it’s going to involve a period of trying to get the libraries configured, which I don’t really have any experience with, and some fooling around trying to get sounds files into the game and trying to play them. I’d really like someone to pair with me on that. Maybe I can get one of my pals to do that. It’s almost as if I have a kind of fear of trying new things. I need a shrink. Or a year in the pen.
Things improve. We’re getting down to the point of diminishing returns for the game and, I think, for the learning that we’re getting from it. Maybe after the holidays, we’ll do something different.
For now, we progress nicely. See you next time!
-
A lunchbox with index cards is better than Jira. My keyboard tray with sticky notes is better than Jira. ↩
-
Jira discourages real person-to-person communication, in favor of “communicating” via the computer. As such, real human interaction is substantially reduced. It’s just not worth it. Sure, keep a record if you think you’ll forget. But not as a communications tool. ↩