GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

Let’s do the drop in animation and the slowing down due to inexplicable friction in outer space’s near complete vacuum. Ooo! A lesson unlearned! I wish I were more wise.

The animation, well, I’m just going to put it in. I apologize.

The spec is: When the ship is first activated, it draws at three times its normal scale, and reduces its size down to normal in one second.

I do this:

var dropScale = 3.0
private var shipGoneFor = 0.0
fun checkIfShipNeeded(deltaTime: Double) {
    if ( ! Ship.active ) {
        shipGoneFor += deltaTime
        if (shipGoneFor > U.ShipDelay) {
            dropScale = 3.0
            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 - 3.0/60.0, 1.0)
    }
}

private fun possiblyDrawFlare(spaceObject: SpaceObject, drawer: Drawer) {
    if (spaceObject.type == SpaceObjectType.SHIP) {
        drawer.scale(dropScale, dropScale)
        if (Controls.accelerate && Random.nextInt(1, 3) == 1) {
            drawer.lineStrip(shipFlare)
        }
    }
}

That works as advertised. I rather like putting the setting and decrementing of dropScale in the check. Putting the use of it i the function called possiblyDrawFlare seems confusing. But it seems to be just the right place logically. Rename that function?

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)
    }
}

I’ll accept that. Let’s fix the magic number 3.0.

var dropScale = U.ShipDropInScale
private 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/60.0, 1.0)
    }
}

Great. Commit: Ship drops in from scale 3 down to 1 in one second.

movie shows ship dropping in

Now to add friction. We want the ship to slow down when not accelerating. There is no known physical explanation for this behavior. Maybe dark matter, I don’t know.

I have no qualms about lifting this from the other version. We are here to compare two approaches, not to reinvent the wheel. In the centralized version we have this:

    private fun accelerateToNewSpeedInOneSecond(vNew:Velocity, vCurrent: Velocity): Velocity {
//        vNew = vCurrent + a*t
//        t = 1
//        a = vNew - vCurrent
        return vNew - vCurrent
    }

    private fun move(deltaTime: Double) {
        position = (position + velocity * deltaTime).cap()
        if (! accelerating ) {
            val acceleration = accelerateToNewSpeedInOneSecond(velocity*U.SHIP_DECELERATION_FACTOR, velocity)*deltaTime
            velocity += acceleration
        }
    }

    const val SHIP_DECELERATION_FACTOR = 0.5 // speed reduces in half every second

So let’s just do something like that in the flat version.

private fun applyControls(spaceObject: SpaceObject, deltaTime: Double) {
    if (Controls.left) spaceObject.angle -= 250.0 * deltaTime
    if (Controls.right) spaceObject.angle += 250.0 * deltaTime
    if (Controls.accelerate) {
        incrementVelocity(spaceObject, Vector2(U.ShipDeltaV, 0.0).rotate(spaceObject.angle) * deltaTime)
    } else {
        val deceleration 
            = accelerateToNewSpeedInOneSecond(spaceObject.velocity*U.ShipDecelerationFactor, spaceObject.velocity)*deltaTime
        Ship.velocity += deceleration
    }
    if (Controls.fire) fireMissile()
}

private fun accelerateToNewSpeedInOneSecond(vNew: Vector2, vCurrent: Vector2): Vector2 {
    return vNew - vCurrent
}

Make that an expression. No, let’s inline it:

private fun applyControls(spaceObject: SpaceObject, deltaTime: Double) {
    if (Controls.left) spaceObject.angle -= 250.0 * deltaTime
    if (Controls.right) spaceObject.angle += 250.0 * deltaTime
    if (Controls.accelerate) {
        incrementVelocity(spaceObject, Vector2(U.ShipDeltaV, 0.0).rotate(spaceObject.angle) * deltaTime)
    } else {
        val deceleration = (spaceObject.velocity * U.ShipDecelerationFactor - spaceObject.velocity) * deltaTime
        Ship.velocity += deceleration
    }
    if (Controls.fire) fireMissile()
}

No, far from clear. Belay that. Rename it:

private fun spaceFrictionPerSecond(vNew: Vector2, vCurrent: Vector2): Vector2 {
    return vNew - vCurrent
}

private fun applyControls(spaceObject: SpaceObject, deltaTime: Double) {
    if (Controls.left) spaceObject.angle -= 250.0 * deltaTime
    if (Controls.right) spaceObject.angle += 250.0 * deltaTime
    if (Controls.accelerate) {
        incrementVelocity(spaceObject, Vector2(U.ShipDeltaV, 0.0).rotate(spaceObject.angle) * deltaTime)
    } else {
        val deceleration 
            = spaceFrictionPerSecond(spaceObject.velocity*U.ShipDecelerationFactor, spaceObject.velocity)*deltaTime
        Ship.velocity += deceleration
    }
    if (Controls.fire) fireMissile()
}

This works. We should really have a test for it. Let’s do one.

    @Test
    fun `ship friction`() {
        createGame(0,0)
        Ship.velocity = Vector2(2.0, 0.0)
        applyControls(Ship, 1.0)
        assertThat(Ship.dx).isEqualTo(1.0, within(0.1))
    }

It goes down by half every second. Test is green. Commit: Space friction reduces speed when not accelerating by 1/2 every second.

movie showing ship slowing down

I think that’ll do. I’m happy to have at least a minimal test for the deceleration. Should I do one for the drop-in?

No. I’m tired and not a very good person and I’m going to let it slide. There are times when you just need a pair to help you do the right thing. And it does work just fine.

Summary

Two new features, neither one done with TDD, but pretty much copied from existing working code. A good practice? Not a great one, but the more tricky one has at least one test and …

Oh, OK, OK, you guilted me into it. I’ll do one for the drop-in. I can probably enhance one of the existing tests.

    @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)
    }

Glory be, the test fails. The code just reduces the scale by a 60th on each call, so my call with one big deltaTime doesn’t do anything good. Change the code to this:

    dropScale = max(dropScale - U.ShipDropInScale*deltaTime, 1.0)

That passes my test and works in game. Commit: added test, fixed issue in dropIn.

That calls the code in the other version into question:

    override fun update(deltaTime: Double, trans: Transaction) {
        accelerating = false
        dropScale -= U.DROP_SCALE/60.0
        if (dropScale < 1.0 ) dropScale = 1.0
        controls.control(this, deltaTime, trans)
        move(deltaTime)
    }

With that value there, the drop in will take one second if we’re cycling at 60 but will vary in time if we’re not. Interesting

Lesson Learned?
No, but what we do see is that when I skip a test, sometimes the code isn’t doing what I think it is. I’ll probably continue to mess up, but a wiser person than I would take this to heart.

In any case, we’re better now than in the other version, we have our two new features, and we can wrap this up.

See you next time!