Kotlin 114: Plugging Away
The Zoom team ‘might have done differently’ but agree that current code isn’t signalling very strongly how it wants to be. I guess today I have to add more of a mess.
Last night everyone seemed to agree that maybe it was premature to move ship and asteroid into a separate class, but that now that things are that way, there’ not an obvious direction for breaking things apart. We figure that if I do more work, a direction may become more clear.
I think today, I’ll start with the missile. In the context of a single kind of flying object, a missile is, well, a flying object that follows a straight trajectory for some period of time (or distance). It will have to get involved in collisions as we grow support for those. A missile is created by a ship. when the fire button is pressed. It is given some small additional velocity over and above that of the ship, in the direction the ship is pointing. Since the ship might be (and usually is) slewing sideways, the missile gets that motion as part of its velocity as well.
We’re getting quite close to needing a universe kind of object that juggles all these flyers. That, too, will help us see how things need to change.
- Strategy
- I should mention a strategy that I plan to use, and it is called, sorry, Strategy. Instead of breaking the flyers out by class, ship, asteroid, and so on, with different subclasses for each kind, I plan instead to provide teach single FlyingObject with one or more Strategy objects that it uses in order to have the unique behavior specific to that type of object. Why?
-
If we do break out by type of flyer, we tend to get a not very cohesive kind of object that contains logic for moving, logic for colliding, logic for drawing, and so on. That is not cohesive, so we’d like to see what happens if we go another way. So we’ll try providing a flyer with a motion strategy, a collision one, and so on. Or something like that: no real commitment as to just what we’ll do. It’s too soon.
- Simple Game
- Bryan pointed out that this is really a very simple game. There are only a few different kinds of things, and they don’t vary in their behavior that much. As such, we may not get a very strong signal as to what to do. We may not see issues that would much more likely arise if there were lots of different kinds of flyers. So … we’ll try to be sensitive to trouble in the code.
Firing a Missile
We have a place where firing a missile looks like the thing to do:
fun update(deltaTime: Double) {
if (controls.left) pointing = pointing + rotationSpeed*deltaTime
if (controls.right) pointing = pointing - rotationSpeed*deltaTime
if (controls.accelerate) {
velocity = limitToSpeedOfLight(velocity + rotatedAcceleration()*deltaTime)
}
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
}
Somewhere near there, we want to check controls.fire and if the missile spawning delay has expired, create a new missile. An issue will arise, which is that here in update, a new thing might appear. A missile might be created, or an asteroid might split. We haven’t sorted out the interface between the universe/game and these things. We have a start, in split
, which returns two asteroids where there used to be one, with the convention being that the caller will remove the old one and add the two that are returned. That scheme won’t work for the missile.
I’m starting to think, vaguely, that the universe will be cycling through all the objects, to tell them to update and to draw. What if the rules are this: as each object updates, by convention it returns zero or more objects, and those objects are to be accumulated into a new collection of the universe’s objects. So normally and object would return itself. If it splits (either into two smaller asteroids, or into a ship and its new missile) it returns two objects. If it dies in the course of things, it returns an empty collection. This would be nearly good, with one minor issue that I can think of: the game would want to know whether there was a live ship, so that if there isn’t, it can spawn a new one, if you have lives left, or restart the game if not.
That seems to be an interesting solution, because in the past I’ve provided update methods to add or remove objects from the universe. Because this is different, I want to try it. Part of why we’re here is to try new things and see how they go.
We don’t even have a game loop yet, but soon. Soon.
We’ll start with the convention that when we call cycle
on an object, it returns a collection of the objects that should be carried forward to the next generation. And we’re here to fire a missile. Let’s write a test.
@Test
fun `ship can fire missile`() {
val controls = Controls()
val ship = FlyingObject.ship(Vector2.ZERO, controls)
controls.fire = true
val flyers = ship.update(tick)
assertThat(flyers.size).isEqualTo(2)
}
IDEA doesn’t think flyers is a collection, since the update function is this:
fun update(deltaTime: Double) {
if (controls.left) pointing = pointing + rotationSpeed*deltaTime
if (controls.right) pointing = pointing - rotationSpeed*deltaTime
if (controls.accelerate) {
velocity = limitToSpeedOfLight(velocity + rotatedAcceleration()*deltaTime)
}
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
}
We change it:
fun update(deltaTime: Double): List<FlyingObject> {
if (controls.left) pointing = pointing + rotationSpeed*deltaTime
if (controls.right) pointing = pointing - rotationSpeed*deltaTime
if (controls.accelerate) {
velocity = limitToSpeedOfLight(velocity + rotatedAcceleration()*deltaTime)
}
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
return listOf(this)
}
This should leave all our other tests running, and this one failing with 1 instead of 2. Well, it would if Controls had fire. Fix that.
class Controls {
var accelerate = false
var left = false
var right = false
var fire = false
}
Test.
expected: 2
but was: 1
Right. Now we’ll just glom up update until it works. This is actually part of our strategy hereL we want to let the FlyingObject class begin to show us how it needs to be refactored, so we’ll make changes where they seem to go, and we’ll not be too aggressive about refactoring … yet.
Ah, this is definitely looking like something needing refactoring now:
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
if (controls.left) pointing = pointing + rotationSpeed*deltaTime
if (controls.right) pointing = pointing - rotationSpeed*deltaTime
if (controls.accelerate) {
velocity = limitToSpeedOfLight(velocity + rotatedAcceleration()*deltaTime)
}
if (controls.fire) {
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT/100.0, 0.0)
val missilePos = position + Vector2(2.0*missileKillRadius, 0.0).rotate(pointing)
val missileVel = velocity + missileOwnVelocity
val missile = FlyingObject(missilePos, missileVel, Vector2.ZERO, missileKillRadius)
result.add(missile)
}
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
result.add(this)
return result
}
I expect the test to pass, but we’ll really want to look hard at the missile’s properties. I’ve played fast and loose here. We’ll reflect in a moment: se need to get to green. Ah, good, green, as expected. Let’s take a breath and think.
Reflection
This update method is complex enough that no matter how much I want to wait to see what happens I feel that I need to fix it. We could readily extract methods, turn update into more of a “composed method” pattern, going something like
fun update {
handleTurns()
handleAcceleration()
handleFiring()
adjustPosition()
}
That would push all kinds of weird methods into FlyingObject and when we read it (h/t Bryan for this notion) when we read it thinking “OK, what does a missile do?” we’d see all these methods like handleTurns that missiles don’t really do at all. Even now, the update is misleading, because we are using the trick of passing in a controls instance that will never change.
Let’s take a tentative step here and push behavior to the Controls object. I’m not sure just how we want this to work, so plan to see some mind-changing as we go forward.
Let’s start with turning. We could imagine two approaches:
- Controls returns an angle that you are supposed to use to rotate the object;
- Controls might send you a message saying to adjust your rotation by n degrees.
I favor the second on the grounds of “tell don’t ask”. We’ll try that.
fun turn(obj: FlyingObject) {
if (left)
}
I get this far and have a new question. We need to scale the turn by deltaTime. So …
fun turn(obj: FlyingObject, deltaTime: Double) {
if (left) obj.turnBy(rotationSpeed*deltaTime)
}
Where do we get rotationSpeed
? Currently it is a property of FlyingObject. I don’t like this, but I’ll use it.
fun turn(obj: FlyingObject, deltaTime: Double) {
if (left) obj.turnBy(obj.rotationSpeed*deltaTime)
if (right) obj.turnBy(-obj.rotationSpeed*deltaTime)
}
This demands turnBy:
fun turnBy(degrees:Double) {
pointing += degrees
}
Now I can do this:
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
controls.turn(this,deltaTime)
I think our turn tests should run now. They do. Let’s break them just to be sure:
fun turn(obj: FlyingObject, deltaTime: Double) {
if (left) obj.turnBy(obj.rotationSpeed*deltaTime +1)
if (right) obj.turnBy(-obj.rotationSpeed*deltaTime)
}
Expecting actual:
91.0
to be close to:
90.0
Perfect. Commit: Controls now respond to turn
and tell received flying object what angle to turnBy.
Now we have, admittedly, made more of a mess in FlyingObject, in that we’ve added a tiny method, and most FlyingObjects don’t care about their pointing variable. We’ll see how things look in due time. Let’s continue with this scheme of pushing behavior to Controls.
val result: MutableList<FlyingObject> = mutableListOf()
controls.turn(this,deltaTime)
controls.accelerate(this, deltaTime)
Last time, I wrote the method and then called it. This time I call it and then write it, because I have a good idea how to do it this time. Well, I think I have …
fun accelerate(obj: FlyingObject, deltaTime: Double) {
if (accelerate) obj.accelerate()
}
In for a penny, I’ll move all the logic over here:
fun accelerate(obj: FlyingObject, deltaTime: Double) {
if (accelerate) {
val deltaV = obj.acceleration.rotate(obj.pointing)
obj.accelerate(deltaV)
}
}
I think I need to get this all in place before I assess it, but it’s not hanging together nicely for me. In F.O.:
fun accelerate(deltaV: Vector2) {
velocity = limitToSpeedOfLight(velocity+deltaV)
}
Test, hopefully. Tests fail:
[rotated velocity y of (7.34788079488412E-15,120.0)]
Expecting actual:
120.0
to be close to:
60.0
by less than 1.0E-4 but difference was 60.0.
[velocity x of (61.0,0.0)]
Expecting actual:
61.0
to be close to:
1.0
by less than 1.0E-4 but difference was 60.0.
Did I forget to take deltaTime into account? Sure did.
fun accelerate(obj: FlyingObject, deltaTime: Double) {
if (accelerate) {
val deltaV = (obj.acceleration*deltaTime).rotate(obj.pointing)
obj.accelerate(deltaV)
}
}
Test even more than hopefully, maybe p-confident for p around a half. Still fails. Enough. Revert do again.
This time we’ll go in smaller steps. First I’ll see what the FO does now:
if (controls.accelerate) {
velocity = limitToSpeedOfLight(velocity + rotatedAcceleration()*deltaTime)
}
OK, let’s extract an accelerate method. First this:
if (controls.accelerate) {
val deltaV = rotatedAcceleration()*deltaTime
velocity = limitToSpeedOfLight(velocity + deltaV)
}
Now:
if (controls.accelerate) {
val deltaV = rotatedAcceleration()*deltaTime
accelerate(deltaV)
}
private fun accelerate(deltaV: Vector2) {
velocity = limitToSpeedOfLight(velocity + deltaV)
}
Now I want the contents of the if to become the accelerate method in Controls. Can IDEA move it for me?
First let’s inline rotatedAcceleration.
if (controls.accelerate) {
val deltaV = acceleration.rotate(pointing) * deltaTime
accelerate(deltaV)
}
Now let’s see if we can move this via a machine refactoring. I don’t find one. Test to be sure that when it breaks, I broke it.
class Controls ...
fun accelerate(obj:FlyingObject, deltaTime: Double) {
if (accelerate) {
val deltaV = obj.acceleration.rotate(obj.pointing) * deltaTime
obj.accelerate(deltaV)
}
}
class FlyingObject ...
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
controls.turn(this,deltaTime)
controls.accelerate(this, deltaTime)
fun accelerate(deltaV: Vector2) {
velocity = limitToSpeedOfLight(velocity + deltaV)
}
Test, very hopefully. Green. Wonder what I did wrong last time. Anyway commit: Controls now respond to accelerate
, tell received FO how much to accelerate by if needed.
Now it becomes more clear what we should do in update:
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
controls.turn(this,deltaTime)
controls.accelerate(this, deltaTime)
if (controls.fire) {
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT/100.0, 0.0)
val missilePos = position + Vector2(2.0*missileKillRadius, 0.0).rotate(pointing)
val missileVel = velocity + missileOwnVelocity
val missile = FlyingObject(missilePos, missileVel, Vector2.ZERO, missileKillRadius)
result.add(missile)
}
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
result.add(this)
return result
}
Clearly we’ll want to move fire over there. Then we’ll have three calls in a row to controls
, which should be consolidated into one. Equally clearly, the fire()
function will want to return a new missile … and, maybe, at some future time, we’ll have it deciding whether to return the existing object or not. Let’s move fire over to controls first and then wonder further about that.
- Following My Nose
- I a just following my nose here. If I have a “grand plan”, it’s no more grand than “move things that Controls could do over to Controls”. I’m really just pushing the pieces over in that direction, and sensing whether I prefer the situation. So far it seems OK. We should wind up with some simplification here in FlyingObject, I think.
I’m just going to replace this if:
if (controls.fire) {
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT/100.0, 0.0)
val missilePos = position + Vector2(2.0*missileKillRadius, 0.0).rotate(pointing)
val missileVel = velocity + missileOwnVelocity
val missile = FlyingObject(missilePos, missileVel, Vector2.ZERO, missileKillRadius)
result.add(missile)
}
I’ll have the function over there return a collection. The alternative would be for turn
to return a FlyingObject?, which I dislike. Make the change in update. (Glutton for punishment? That’s what I did last time and had to roll back.)
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
controls.turn(this,deltaTime)
controls.accelerate(this, deltaTime)
val missiles = controls.fire(this, deltaTime)
result.addAll(missiles)
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
return result
}
fun fire(obj: FlyingObject, deltaTime: Double): List<FlyingObject> {
if (fire) {
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT/100.0, 0.0)
val missilePos = obj.position + Vector2(2.0*missileKillRadius, 0.0).rotate(obj.pointing)
val missileVel = obj.velocity + missileOwnVelocity
val missile = FlyingObject(missilePos, missileVel, Vector2.ZERO, missileKillRadius)
return listOf(missile)
}
return listOf()
}
Test. Should work, I think. But no:
expected: 2
but was: 1
Oh, never mind. When I rearranged update
I forgot to add this
into the result.
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
controls.turn(this,deltaTime)
controls.accelerate(this, deltaTime)
val missiles = controls.fire(this, deltaTime)
result.addAll(missiles)
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
result.add(this)
return result
}
Green. Commit: Controls now handles all turns, acceleration, and creation of missiles (with no refractory period).
Let’s consolidate the calls to controls and then look around.
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
val additions = controls.control(this, deltaTime)
result.addAll(additions)
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
result.add(this)
return result
}
fun control(obj:FlyingObject, deltaTime: Double): List<FlyingObject> {
turn(obj,deltaTime)
accelerate(obj,deltaTime)
return fire(obj,deltaTime)
}
Test. Green. Commit: FlyingObject.update just calls Controls.control, adjusts position, and returns collection of this and any returned objects.
Time to reflect and the message gives me a hint of something to reflect about. Here’s update:
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
val additions = controls.control(this, deltaTime)
result.addAll(additions)
val proposedPosition = position + velocity*deltaTime
position = cap(proposedPosition)
result.add(this)
return result
}
We’re just guessing about the return. We use it in tests but not in the game, since we have no game proper. It really seems odd that we do everything in controls except adjust position. Let’s move that over as well.
I’m not sure whether the updating of position should be in controls or not. For now, let’s extract it to a method in FO:
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
val additions = controls.control(this, deltaTime)
result.addAll(additions)
move(deltaTime)
result.add(this)
return result
}
private fun FlyingObject.move(deltaTime: Double) {
position = cap(position + velocity * deltaTime)
}
Reflection
Our FlyingObject looks like this now:
const val SPEED_OF_LIGHT = 5000.0
class FlyingObject(
var position: Vector2,
var velocity: Vector2,
val acceleration: Vector2,
killRad: Double,
splitCt: Int = 0,
private val controls: Controls = Controls()
) {
var killRadius = killRad
private set
var pointing: Double = 0.0
var rotationSpeed = 360.0
var splitCount = splitCt
fun accelerate(deltaV: Vector2) {
velocity = limitToSpeedOfLight(velocity + deltaV)
}
private fun asSplit(): FlyingObject {
splitCount -= 1
killRadius /= 2.0
velocity = velocity.rotate(random() * 360.0)
return this
}
private fun asTwin() = asteroid(
pos = position,
vel = velocity.rotate(random() * 360.0),
killRad = killRadius,
splitCt = splitCount
)
fun cycle(drawer: Drawer, seconds: Double, deltaTime: Double) {
drawer.isolated {
update(deltaTime)
draw(drawer)
}
}
private fun draw(drawer: Drawer) {
val center = Vector2(drawer.width/2.0, drawer.height/2.0)
drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
drawer.translate(position)
drawer.rectangle(-killRadius /2.0,-killRadius /2.0, killRadius, killRadius)
}
private fun cap(v: Vector2): Vector2 {
return Vector2(cap(v.x), cap(v.y))
}
private fun cap(coord: Double): Double {
return (coord+10000.0)%10000.0
}
fun collides(other: FlyingObject):Boolean {
val dist = position.distanceTo(other.position)
val allowed = killRadius + other.killRadius
return dist < allowed
}
private fun limitToSpeedOfLight(v: Vector2): Vector2 {
val speed = v.length
if (speed < SPEED_OF_LIGHT) return v
else return v*(SPEED_OF_LIGHT/speed)
}
private fun FlyingObject.move(deltaTime: Double) {
position = cap(position + velocity * deltaTime)
}
fun split(): List<FlyingObject> {
if (splitCount < 1) return listOf()
val meSplit = asSplit()
val newGuy = meSplit.asTwin()
return listOf(meSplit, newGuy)
}
fun turnBy(degrees:Double) {
pointing += degrees
}
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
val additions = controls.control(this, deltaTime)
result.addAll(additions)
move(deltaTime)
result.add(this)
return result
}
companion object {
fun asteroid(pos:Vector2, vel: Vector2, killRad: Double = 1000.0, splitCt: Int = 2): FlyingObject {
return FlyingObject(
position = pos,
velocity = vel,
acceleration = Vector2.ZERO,
killRad = killRad,
splitCt = splitCt
)
}
fun ship(pos:Vector2, control:Controls= Controls()): FlyingObject {
return FlyingObject(
position = pos,
velocity = Vector2.ZERO,
acceleration = Vector2(60.0, 0.0),
killRad = 100.0,
controls = control
)
}
}
}
Quite a lot, especially since it’s the only class in the system other than the tiny Controls.
Some of these methods don’t seem to me to have much to do with FlyingObject, notable the cap functions. Let’s move a few more constants up to the top and see what we can do.
const val UNIVERSE_SIZE = 10000.0
private fun cap(v: Vector2): Vector2 {
return Vector2(cap(v.x), cap(v.y))
}
private fun cap(coord: Double): Double {
return (coord + UNIVERSE_SIZE)% UNIVERSE_SIZE
}
We see feature envy on these two. The first wants to be a method on Vector2 and the second on Double. Kotlin provides.
fun Vector2.cap(): Vector2 {
return Vector2(this.x.cap(), this.y.cap())
}
fun Double.cap(): Double {
return (this + UNIVERSE_SIZE) % UNIVERSE_SIZE
}
And the appropriate changes in FO:
private fun FlyingObject.move(deltaTime: Double) {
position = (position + velocity * deltaTime).cap()
}
And we can delete the cap methods in FO. This also seems like feature envy:
private fun limitToSpeedOfLight(v: Vector2): Vector2 {
val speed = v.length
if (speed < SPEED_OF_LIGHT) return v
else return v*(SPEED_OF_LIGHT/speed)
}
Now we might argue here that we need a game class, Velocity, but for now, it’s a Vector2 and we’ll honor that.
fun Vector2.limitedToLightSpeed(): Vector2 {
val speed = this.length
return if (speed < SPEED_OF_LIGHT) this
else this*(SPEED_OF_LIGHT/speed)
}
I thought the word limited
was better here, as used:
fun accelerate(deltaV: Vector2) {
velocity = (velocity + deltaV).limitedToLightSpeed()
}
Now FlyingObject is a bit simpler:
class FlyingObject(
var position: Vector2,
var velocity: Vector2,
val acceleration: Vector2,
killRad: Double,
splitCt: Int = 0,
private val controls: Controls = Controls()
) {
var killRadius = killRad
private set
var pointing: Double = 0.0
var rotationSpeed = 360.0
var splitCount = splitCt
fun accelerate(deltaV: Vector2) {
velocity = (velocity + deltaV).limitedToLightSpeed()
}
private fun asSplit(): FlyingObject {
splitCount -= 1
killRadius /= 2.0
velocity = velocity.rotate(random() * 360.0)
return this
}
private fun asTwin() = asteroid(
pos = position,
vel = velocity.rotate(random() * 360.0),
killRad = killRadius,
splitCt = splitCount
)
fun cycle(drawer: Drawer, seconds: Double, deltaTime: Double) {
drawer.isolated {
update(deltaTime)
draw(drawer)
}
}
private fun draw(drawer: Drawer) {
val center = Vector2(drawer.width/2.0, drawer.height/2.0)
drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
drawer.translate(position)
drawer.rectangle(-killRadius /2.0,-killRadius /2.0, killRadius, killRadius)
}
fun collides(other: FlyingObject):Boolean {
val dist = position.distanceTo(other.position)
val allowed = killRadius + other.killRadius
return dist < allowed
}
private fun move(deltaTime: Double) {
position = (position + velocity * deltaTime).cap()
}
fun split(): List<FlyingObject> {
if (splitCount < 1) return listOf()
val meSplit = asSplit()
val newGuy = meSplit.asTwin()
return listOf(meSplit, newGuy)
}
fun turnBy(degrees:Double) {
pointing += degrees
}
fun update(deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
val additions = controls.control(this, deltaTime)
result.addAll(additions)
move(deltaTime)
result.add(this)
return result
}
I hate the word pointing
. rename to heading
. Oh we’re way behind on commits, too. Fortunately still green. Commit: Clean up FlyingObject, create extensions for Vector2 and Double. And this is nicer:
fun turnBy(degrees:Double) {
heading += degrees
}
IDEA observes that all but the control
method in Controls can be private. That’s a good thing to deal with because I think we’re about to come up with alternate controls.
It also observes that we don’t use deltaTime
in fire
, but I think that’ll change. Leave it.
In fact, let’s write a test for that. We want missiles to fire no faster than … ah … in my prior implementation, you had to lift the fire button and fire again, which limited fire to a couple per second unless you had a bump stock on your fire button. And you’d better [DELETED] not. Let’s do it that way. We’ll write the test accordingly:
@Test
fun `can only fire once per press`() {
val controls = Controls()
val ship = FlyingObject.ship(Vector2.ZERO, controls)
controls.fire = true
var flyers = ship.update(tick)
assertThat(flyers.size).isEqualTo(2)
flyers = ship.update(tick)
assertThat(flyers.size).isEqualTo(1)
controls.fire = false
flyers = ship.update(tick)
assertThat(flyers.size).isEqualTo(1)
controls.fire = true
flyers = ship.update(tick)
assertThat(flyers.size).isEqualTo(2)
}
This should fail with a 2 expecting 1:
expected: 1
but was: 2
So in Controls:
private fun fire(obj: FlyingObject, deltaTime: Double): List<FlyingObject> {
if (!fire) {
holdFire = false
} else {
if (!holdFire) {
holdFire = true
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 100.0, 0.0)
val missilePos = obj.position + Vector2(2.0 * missileKillRadius, 0.0).rotate(obj.heading)
val missileVel = obj.velocity + missileOwnVelocity
val missile = FlyingObject(missilePos, missileVel, Vector2.ZERO, missileKillRadius)
return listOf(missile)
}
}
return listOf()
}
This is a bit nasty but let’s make it work. The hold fire test passes. Improve this method:
private fun fire(obj: FlyingObject, deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
if (!fire) {
holdFire = false
} else {
if (!holdFire) {
holdFire = true
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 100.0, 0.0)
val missilePos = obj.position + Vector2(2.0 * missileKillRadius, 0.0).rotate(obj.heading)
val missileVel = obj.velocity + missileOwnVelocity
val missile = FlyingObject(missilePos, missileVel, Vector2.ZERO, missileKillRadius)
result.add(missile)
}
}
return result
}
Test. Good. Extract method:
private fun fire(obj: FlyingObject, deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
if (!fire) {
holdFire = false
} else {
if (!holdFire) {
holdFire = true
result.add(createMissile(obj))
}
}
return result
}
private fun createMissile(obj: FlyingObject): FlyingObject {
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 100.0, 0.0)
val missilePos = obj.position + Vector2(2.0 * missileKillRadius, 0.0).rotate(obj.heading)
val missileVel = obj.velocity + missileOwnVelocity
return FlyingObject(missilePos, missileVel, Vector2.ZERO, missileKillRadius)
}
Test. Green. Commit: Missile firing now requires button lift after each firing.
I still don’t love this:
private fun fire(obj: FlyingObject, deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
if (!fire) {
holdFire = false
} else {
if (!holdFire) {
holdFire = true
result.add(createMissile(obj))
}
}
return result
}
Let’s see if we can rearrange this more nicely.
private fun fire(obj: FlyingObject, deltaTime: Double): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
if (fire && !holdFire ) result.add(createMissile(obj))
holdFire = fire
return result
}
I like this for being compact, but freely grant that it is a bit tricky to think about. If we enter with fire true and holdFire false, we’ll fire and set holdFire to true, because fire is true. If we later come by with fire still held down, we won’t fire, and we’ll set holdFire (again) to true. If we ever come by with fire false, we won’t fire, and we’ll set holdFire false.
I’m going to keep this on the grounds that it is nice, and admit that it needs a little thinking about. I’ll submit it to my reviewers for comment. The method doesn’t need deltaTime, still, and now I’ll remove it.
private fun fire(obj: FlyingObject): List<FlyingObject> {
val result: MutableList<FlyingObject> = mutableListOf()
if (fire && !holdFire ) result.add(createMissile(obj))
holdFire = fire
return result
}
Green. Commit: Make fire
simpler.
I think we’re done for today. We’ve implemented some new capability, at least the firing requiring a button lift and re-press.
We’ve moved the logic for whether we turn or accelerate or fire over to Controls, which call back with elementary methods on the FlyingObject that do turnBy
, or accelerate
, and the FlyingObject by convention returns one or more objects, intended to be in the universe. (In future, it’ll be zero or more.)
Things are more clean, I think, and I suggest that it’s easy to see that for different kinds of objects, we can provide different kinds of controls. The ship’s controller will have all this fancy turning and accelerating and firing, while the asteroid’s controller will be pretty empty.
At this moment, I’m not clear in my mind how we’ll deal with collisions. What I think might happen is that we’ll provide a collider object for each type of FlyingObject and when we get around to colliding, a given object will scan all the other objects to see if any want to collide with it and then they’ll act accordingly. (Here, “accordingly” means “in some fashion yet to be determined but I’m sure it’ll be just fine”.)
Overall the world is a better place and facts and capabilities are beginning to sort out into sensible clumps. That’s how evolutionary development works.
Anyway a good morning’s work. See you next time!