Kotlin 136
Let’s do hyperspace … and try to do it right.
Last time, we made some changes to what is now the ISpaceObject interface and the classes that implement it, removing four of the seven member variables / properties that the interface required. That’s a noticeable improvement. That will make at least some upcoming features easier: I have in mind the saucer and its missiles.
Today, however, I have in mind finishing hyperspace. When we hit the space bar1, our ship pops into hyperspace, and pops out a while later, somewhere else entirely. If I recall correctly, and a quick scan of the original Asteroids source confirms this, there is a small chance that you will explode as you emerge from hyperspace. I believe there may also be a refractory period between jumps, while your hyperspace generators recharge or something. Today, the chance of explosion is on our menu.
In Kotlin 134.5 we got started on this, changing the WaitingForSafety
state in update
in ShipMonitor
from this:
WaitingForSafety -> {
if (safeToEmerge) {
toBeCreated = listOf(shipReset())
HaveSeenShip
} else {
startCheckingForSafeEmergence()
WaitingForSafety
}
}
To this:
WaitingForSafety -> {
if (safeToEmerge) {
val ship = shipReset()
val ret = mutableListOf(ship)
if (random(0.0, 1.0) > 0.90) {
val destroyer = Flyer.asteroid(ship.position, Velocity.ZERO, 100.0, 0)
val splat = Flyer.splat(destroyer)
ret.add(destroyer)
ret.add(splat)
}
toBeCreated = ret
HaveSeenShip
} else {
startCheckingForSafeEmergence()
WaitingForSafety
}
}
The new patch of code, in the if
, rolls the dice and if the odds are not in your favor, it adds two objects into the toBeCreated
list. One of them is a splat, to make a little explosion, and the other is an asteroid named destroyer, set to emerge right on top of the ship. When those are added, the next thing that happens is that the collision code runs, the destroyer and the ship are destroyed, and the splat makes a splat.
I should mention that the ship gets a special kind of splat for when it explodes, which we’ll have to code up in due time. I just used the splat to test the effect.
The code above was a spike and I rolled back to the version shown first. Our mission today will be to create something that is more fit for prime time. There are these issues:
- Determine whether the ship is in hyperspace, or whether it has been destroyed. This effect only applies when you’re using hyperspace2.
- Make creation of the destroyer seem different from creating an asteroid, even though it may use a similar mechanism.
- Hm. Would rezzing a missile on top of ourselves do the job? It probably would. The issue there is that missiles are created with an offset from the ship. We’d have to undo all those settings.
- Try to get the code to be as clear as possible, preferably more clear than what we did in the spike.
That should be enough to worry about. Other notions will appear. One of them is that the cat wants brekkers. OK, cat starvation averted, probably.
I think we’d do well to test this. Otherwise, what do we really stand for around here? This will be a ShipMonitor test, and maybe it should look like this:
@Test
fun `hyperspace emergence`() {
val tick = 0.01
val controls = Controls()
val ship = SolidObject.ship(Point(10.0, 10.0), controls)
val mon = ShipMonitor(ship)
controls.hyperspace = true
mon.hyperspaceFatal = true
mon.safeToEmerge = true
mon.state = ShipMonitorState.WaitingForSafety
val created = mon.update(tick)
assertThat(created.size).isEqualTo(3)
}
This seems to me to tell the story, though I’m not sure about some of it. We’ll check up on the details. The idea is that we set the controls to hyperspace, which may be wrong … the player could have lifted off the space bar by now. We may need to check something other than that flag. We’ll find out.
We set a new flag in ShipMonitor, hyperspaceFatal
, to be true if the emergence is to be fatal. We set the monitor in WaitingForSafety
mode and set safeToEmerge
. Ready to go. We update and expect to get our ship, the destroyer, and the splat back.
If I add the hyperspaceFatal flag to ShipMonitor, the test should execute and fail.
class ShipMonitor(val ship: SolidObject) : ISpaceObject {
override var elapsedTime: Double = 0.0
var state = HaveSeenShip
var safeToEmerge = false
var hyperspaceFatal = false
Test, expecting this to come back 1 instead of 3.
expected: 3
but was: 1
Perfect. Let’s see what we can find out about the controls.hyperspace
flag.
fun control(ship: SolidObject, deltaTime: Double): List<ISpaceObject> {
if (hyperspace) {
val vel = Velocity(1000.0, 0.0).rotate(random(0.0,360.0))
val destroyer = SolidObject(
killRadius = 100.0,
position = ship.position,
velocity = Velocity.ZERO
)
return listOf(destroyer)
}
turn(ship, deltaTime)
accelerate(ship, deltaTime)
return fire(ship)
}
There’s a convenient destroyer that obviously works. Let’s remember to use it inside ShipMonitor as well. How is this flag used? I think that we’ll find that it is checked in the ShipFinalizer.
class ShipFinalizer : IFinalizer {
override fun finalize(solidObject: SolidObject): List<ISpaceObject> {
if ( solidObject.deathDueToCollision())
solidObject.position = U.CENTER_OF_UNIVERSE
else
solidObject.position = U.randomPoint()
return emptyList()
}
}
Hm, how does deathDueToCollision()
work? And can we use it in the ShipMonitor?
fun deathDueToCollision(): Boolean {
return !controls.hyperspace
}
OK that seems good, but I have a concern.
- Concern
- The hyperspace flag in controls goes true when you hit the space bar and false when you lift off of it. So it seems that if you held down space bar you might just pop right back into hyperspace. that could be a problem. I make a note to think about it.
I think we are ready to implement our feature. I think I’ll put it in procedurally and then refactor to make it better once it works. Ah, no, look how ready the code is for a more complex method there on the third line.
WaitingForSafety -> {
if (safeToEmerge) {
toBeCreated = listOf(shipReset())
HaveSeenShip
} else {
startCheckingForSafeEmergence()
WaitingForSafety
}
}
So … extract method …
WaitingForSafety -> {
if (safeToEmerge) {
toBeCreated = makeEmergenceObjects()
HaveSeenShip
} else {
startCheckingForSafeEmergence()
WaitingForSafety
}
}
private fun makeEmergenceObjects() = listOf(shipReset())
Now convert that method to block body:
private fun makeEmergenceObjects(): List<ISpaceObject> {
return listOf(shipReset())
}
Now what? On my mind is the fact that I’ve posited that hyperspaceFatal
flag, and it needs to get set somewhere reasonable. That aside, we have two cases, deathDueToCollision
and not. So:
private fun makeEmergenceObjects(): List<ISpaceObject> {
return when (ship.deathDueToCollision() or !hyperspaceFatal) {
true -> {
listOf(shipReset())
}
false -> {
val splat = SolidObject.splat(ship)
val destroyer = SolidObject(
killRadius = 100.0,
position = ship.position,
velocity = Velocity.ZERO
)
listOf(splat, destroyer, shipReset())
}
}
}
I think this should pass the test. If it doesn’t, we learn something. It passes! Excellent.
Now how can we get the hyperspaceFatal
flag set randomly? And we should think about reversing the sense of hyperspaceFatal
since we’re “notting” it in the code above.
Since we’re calling update
and have the fatality flag already set, how can we set it and leave our test running? Let’s do this:
WaitingForSafety -> {
if (safeToEmerge) {
toBeCreated = makeEmergenceObjects()
val HYPERSPACE_DEATH_PROBABILITY = 0.5
hyperspaceFatal = random(0.0, 1.0) < HYPERSPACE_DEATH_PROBABILITY
HaveSeenShip
} else {
startCheckingForSafeEmergence()
WaitingForSafety
}
}
We’ll set the flag at emergence, so that its usual use is for “next time”. Test, then test in the game to see some destruction.
In the game, hyperspace works but since I lift my finger from the hyperspace bar, the check for death by collision fails in makeEmergenceObjects()
. Try this. We’ll check whether the ship is set to emerge at center, in which case either he has rolled a random position to screen center, or, almost certainly, he’s coming back from an untimely death.
return when ((ship.position == U.CENTER_OF_UNIVERSE) or !hyperspaceFatal) {
...
Game test shows this to be working. Let’s move that constant into U and set it to a reasonable value. A quick look at my Codea version makes me think that I didn’t implement a chance of death from hyperspace. Bryan thinks it was just due to rezzing too near an asteroid. I’ll have to research that. For now, let’s set it to ten percent and review our code:
object U {
const val HYPERSPACE_DEATH_PROBABILITY = 0.1
WaitingForSafety -> {
if (safeToEmerge) {
toBeCreated = makeEmergenceObjects()
hyperspaceFatal = random(0.0, 1.0) < U.HYPERSPACE_DEATH_PROBABILITY
HaveSeenShip
} else {
startCheckingForSafeEmergence()
WaitingForSafety
}
}
private fun makeEmergenceObjects(): List<ISpaceObject> {
return when ((ship.position == U.CENTER_OF_UNIVERSE) or !hyperspaceFatal) {
true -> {
listOf(shipReset())
}
false -> {
val splat = SolidObject.splat(ship)
val destroyer = SolidObject(
killRadius = 100.0,
position = ship.position,
velocity = Velocity.ZERO
)
listOf(splat, destroyer, shipReset())
}
}
}
If we were to rename hyperspaceFatal
to nextHyperspaceFatal
, that might communicate better. I like the fact that it’s initialized to false for two reasons. First, it lets us test it without providing a fake random number. Second, it ensures that the first time you use hyperspace you don’t die.
Let’s do the rename. OK, what else? Let’s have a destroyer method in our companion object:
fun shipDestroyer(ship: SolidObject): SolidObject {
return SolidObject(
killRadius = 100.0,
position = ship.position,
velocity = Velocity.ZERO
)
}
Now we can use that in ShipMonitor and Controls.
private fun makeEmergenceObjects(): List<ISpaceObject> {
return when ((ship.position == U.CENTER_OF_UNIVERSE) or !nextHyperspaceFatal) {
true -> {
listOf(shipReset())
}
false -> {
val splat = SolidObject.splat(ship)
val destroyer = SolidObject.shipDestroyer(ship)
listOf(splat, destroyer, shipReset())
}
}
}
fun control(ship: SolidObject, deltaTime: Double): List<ISpaceObject> {
if (hyperspace) {
return listOf(SolidObject.shipDestroyer(ship))
}
turn(ship, deltaTime)
accelerate(ship, deltaTime)
return fire(ship)
}
Test. Green. Commit: hyperspace has 0.1 chance of death on emergence.
Reflection
A few issues come to mind:
- The determination of whether we’re returning the ship from hyperspace is kind of a trick: we look to see if it is returning at CENTER_OF_UNIVERSE. That’s not as direct as we might like. We could have a more persistent ship state but then we’d have to deal with who sets it and who clears it.
- If the ship emerges too close to an edge, it is hard to spot. In my Lua game, I emerge a bit in from the edges, probably for that reason.
- The splat isn’t enough of an explosion if you’re not looking for it. The ship should have a more impressive explosion.
- I also wonder how to play sounds in here. That would be a nice addition, some bangs and booms and thumps. Pew pew. You know the drill.
While this works well, it’s perhaps the most complex interaction in the system so far, and it does seem a bit ragged:
- Controls sees hyperspace key and rezzes ship destroyer on ship.
- Ship collides with destroyer, both are removed from the mix.
- ShipFinalizer asks ship why it was destroyed, ship sees hyperspace key still down and says it wasn’t a collision. Finalizer sets a random return position, else CENTER_OF_UNIVERSE.
- ShipMonitor notices ship is gone, waits a discreet interval, then puts the ship back into the mix. If it’s not going to CENTER, monitor knows its a hyperspace return and if the odds are against the ship, also rezzes another destroyer and a splat for effect.
Part of this is simply the way our distributed intelligence works, but the handling of the hyperspace flag, in particular, troubles me. Probably we should set a more stable flag in the ship and clear it as needed. We’ll look at this in the future.
Overall, though, I am satisfied. Let’s ship it. No pun intended. And I’ll see you next time!