GitHub Decentralized Repo
GitHub Centralized Repo GitHub Flat Repo

I think we’ll do a bit on the ship. That will require me to think a bit about coding standards up in this um program. In Post: This is actually getting interesting!

First, I want to check a possible error in the centralized version. Ah, good, no error. I woke up this morning afraid that in limiting a ship to its top speed, I might have just limited the x and y values, which would of course not work. But in fact, I have this:

    fun Velocity.limitedToLightSpeed(): Velocity {
        val speed = this.length
        return if (speed < U.SPEED_OF_LIGHT) this
        else this*(U.SPEED_OF_LIGHT/speed)
    }

So that’s good. And of course we’ll need this in our current procedural version. I fully intend to lift code like this from the other versions. I’m here to find out what a procedural version looks like, not to reinvent every wheel, if in fact there are any wheels in the program.

Coding Standard

It seems to me that it’s reasonable not to put the whole program in one big file. There is a natural breakdown of things, such as into asteroids, missiles, saucer and ship, and it seems sensible to keep that code together even though we are, for some bizarre reason, not creating real objects. So let’s see if we can write some tests for a ship and then create one.

I’ll keep the tests in the same test file for now. No, I immediately hate that idea, I’ll create a new file of Ship tests. I can’t bring myself to be entirely stupid about organizing things.

OK, this is truly silly. Here’s my first test:

class ShipTests {
    @Test
    fun `ship can move`() {
        val ship = Ship(100.0, 150.0, 10.0, 15.0)
        moveShip(ship, 200.0, 200.0, 0.1)
        assertThat(ship.x).isEqualTo(101.0, within(0.01))
        assertThat(ship.y).isEqualTo(151.5, within(0.01))
    }
}

And my implementation of Ship:

data class Ship(var x: Double, var y: Double, var dx: Double, var dy: Double) { }

fun moveShip(ship: Ship, width: Double, height: Double, deltaTime: Double) {
    with(ship) {
        x += dx * deltaTime
        if (x > width) x -= width
        if (x < 0) x += width
        y += dy * deltaTime
        if (y > height) y -= width
        if (y < 0) y += width
    }
}

Now of course I copied the moveAsteroid code, which is where all that additional capability came from:

data class Asteroid(var x: Double, var y: Double, var dx: Double, var dy: Double)

fun moveAsteroid(asteroid: Asteroid, width: Double, height: Double, deltaTime: Double) {
    with (asteroid) {
        x += dx*deltaTime
        if ( x > width ) x -= width
        if ( x < 0 ) x += width
        y += dy*deltaTime
        if ( y > height ) y -= width
        if ( y < 0 ) y += width
    }
}

There’s no real difference. The code’s the same and even a procedural programmer from the 1980s would want to eliminate that duplication. But maybe it’s too soon, what about the drawing? What about the kill radius and the asteroid scale? I don’t know whether to wait, or to consolidate this code now.

I think no rational being could leave it like this. I shall emulate a rational being and eliminate the duplication.

I’ll rename the asteroid to SpaceObject and work with it throughout.

data class SpaceObject(var x: Double, var y: Double, var dx: Double, var dy: Double)

fun drawAsteroid(spaceObject: SpaceObject, drawer: Drawer) {
    drawer.isolated {
        drawer.translate(spaceObject.x, spaceObject.y)
        drawer.scale(4.00, 4.0)
        drawer.stroke = ColorRGBa.WHITE
        drawer.lineStrip(asteroidPoints)
    }
}

fun moveAsteroid(spaceObject: SpaceObject, width: Double, height: Double, deltaTime: Double) {
    with (spaceObject) {
        x += dx*deltaTime
        if ( x > width ) x -= width
        if ( x < 0 ) x += width
        y += dy*deltaTime
        if ( y > height ) y -= width
        if ( y < 0 ) y += width
    }
}

Now rename moveAsteroid to moveSpaceObject, I guess. Or how about just move.

fun move(spaceObject: SpaceObject, width: Double, height: Double, deltaTime: Double) {
    with (spaceObject) {
        x += dx*deltaTime
        if ( x > width ) x -= width
        if ( x < 0 ) x += width
        y += dy*deltaTime
        if ( y > height ) y -= width
        if ( y < 0 ) y += width
    }
}

Now I don’t really need my ship test at all. Remove it.

Hm. Now I’m back where I was. Weird, but perhaps good.

Let’s first do drawShip. I think I’ll put that right into the SpaceObject file.

I move over the points, renaming Point to Vector2 and renaming the variables:

private val shipPoints = listOf(
    Vector2(-3.0, -2.0), Vector2(-3.0, 2.0), Vector2(-5.0, 4.0),
    Vector2(7.0, 0.0), Vector2(-5.0, -4.0), Vector2(-3.0, -2.0)
)

private val shipFlare = listOf(
    Vector2(-3.0,-2.0), Vector2(-7.0,0.0), Vector2(-3.0, 2.0)
)

Then I can do a simple drawShip:

fun drawShip(spaceObject: SpaceObject, drawer: Drawer) {
    drawer.isolated {
        drawer.translate(spaceObject.x, spaceObject.y)
        drawer.scale(4.00, 4.0)
        drawer.stroke = ColorRGBa.WHITE
        drawer.lineStrip(shipPoints)
    }
}

And in the main, draw that instead of an asteroid, and we get this:

screen showing ship drawn with thick lines

Again the lines are too thick, no surprise there. I’m not sure about the scale. We’ll have to tune those things, but all in due time.

The ship needs to be rotated and in fact in the other versions, the asteroids have a rotation as well, so that they’ll look a bit more different from one another. So we can add a rotation to the space object. I don’t see much to test there, so I’ll just do it.

data class SpaceObject(
    var x: Double,
    var y: Double,
    var dx: Double,
    var dy: Double,
    var angle: Double = 0.0
)

We might want some constructors but not yet. Let’s set the ship angle in the main and then see if we can make it happen.

val spaceObject = SpaceObject(
            512.0,
            512.0,
            100.0,
            -90.0,
            45.0
        )

And in drawShip, maybe this:

fun drawShip(spaceObject: SpaceObject, drawer: Drawer) {
    drawer.isolated {
        drawer.translate(spaceObject.x, spaceObject.y)
        drawer.scale(4.00, 4.0)
        drawer.rotate(spaceObject.angle)
        drawer.stroke = ColorRGBa.WHITE
        drawer.lineStrip(shipPoints)
    }
}

I rediscover that positive angles turn one to the right in OPENRNDR, but that’s consistent with what we found in the other programs.

ship pointing down at 45 degree angle

Again, I’ll quit while I’m ahead, after just a few small steps. So far this is quite interesting, more interesting than I’d have thought. Let’s sum up.

Summary

In this procedural version, the similarity between Asteroid and Ship (so far) is so obvious that I couldn’t resist removing it. In the object-oriented version, every object has its own move:

class Ship

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

class Asteroid

    override fun update(deltaTime: Double, trans: Transaction) {
        position = (position + velocity * deltaTime).cap()
    }

class Missile

    override fun update(deltaTime: Double, trans: Transaction) {
        timeOut.execute(trans)
        position = (position + velocity * deltaTime).cap()
    }

And so on. So far, we have no such difference showing up in the moving, though we may find differences arising later on. It’ll be interesting to see how we handle them.

I’m wondering … will this procedural version turn out to be substantially more compact than the more object-oriented ones? And if so … will there be good ways to get similar compaction in the OO ones?

Despite these points of interest, I’m finding it pretty hard to stay motivated. Partly that’s just because I’m a bit burned out on Asteroids, even though there is learning to be had here. Partly it’s because I find it very tiring to try to think without objects right there on the surface of my mind. Or maybe it’s the weather or something.

All those drags aside, this is turning interesting sooner than I thought it would, with a whole “object” coming down to just a draw function … and I think we could eliminate some duplication there as well. Look at these:

fun drawAsteroid(spaceObject: SpaceObject, drawer: Drawer) {
    drawer.isolated {
        drawer.translate(spaceObject.x, spaceObject.y)
        drawer.scale(4.00, 4.0)
        drawer.stroke = ColorRGBa.WHITE
        drawer.lineStrip(asteroidPoints)
    }
}

fun drawShip(spaceObject: SpaceObject, drawer: Drawer) {
    drawer.isolated {
        drawer.translate(spaceObject.x, spaceObject.y)
        drawer.scale(4.00, 4.0)
        drawer.rotate(spaceObject.angle)
        drawer.stroke = ColorRGBa.WHITE
        drawer.lineStrip(shipPoints)
    }
}

The asteroid version could (and should) rotate. The only difference here, so far, is the different list of points. We can do this. Click on shipPoints, refactor Introduce Parameter:

fun drawShip(spaceObject: SpaceObject, drawer: Drawer, points: List<Vector2>) {
    drawer.isolated {
        drawer.translate(spaceObject.x, spaceObject.y)
        drawer.scale(4.00, 4.0)
        drawer.rotate(spaceObject.angle)
        drawer.stroke = ColorRGBa.WHITE
        drawer.lineStrip(points)
    }
}

Rename that function to draw, and write a new one:

fun drawShip(spaceObject: SpaceObject, drawer: Drawer) {
    draw(spaceObject, drawer, shipPoints)
}

fun draw(spaceObject: SpaceObject, drawer: Drawer, points: List<Vector2>) {
    drawer.isolated {
        drawer.translate(spaceObject.x, spaceObject.y)
        drawer.scale(4.00, 4.0)
        drawer.rotate(spaceObject.angle)
        drawer.stroke = ColorRGBa.WHITE
        drawer.lineStrip(points)
    }
}

This should work as before. It does. Change drawAsteroid:

fun drawAsteroid(spaceObject: SpaceObject, drawer: Drawer) {
    draw(spaceObject, drawer, asteroidPoints)
}

Let’s add an asteroid in the main:

    program {
//        val image = loadImage("data/images/pm5544.png")
        val font = loadFont("data/fonts/default.otf", 64.0)
        val ship = SpaceObject(
            512.0,
            512.0,
            100.0,
            -90.0,
            45.0
        )
        val asteroid = SpaceObject(
            300.0,
            300.0,
            74.0,
            40.0
        )
        var lastTime = 0.0
        var deltaTime = 0.0
        keyboard.keyDown.listen {
            when (it.name) {
                "d" -> {controls_left = true}
                "f" -> {controls_right = true}
                "j" -> {controls_accelerate = true}
                "k" -> {controls_fire = true}
                "space" -> {controls_hyperspace = true}
//                "q" -> { insertQuarter()}
            }
        }
        keyboard.keyUp.listen {
            when (it.name) {
                "d" -> {controls_left = false}
                "f" -> {controls_right = false}
                "j" -> {controls_accelerate = false}
                "k" -> {controls_fire = false}
                "space" -> {
                    controls_hyperspace = false
                }
            }
        }

        extend {
            drawer.fill = ColorRGBa.WHITE
            drawer.stroke = ColorRGBa.RED
            deltaTime = seconds - lastTime
            lastTime = seconds
            if (controls_accelerate) {
                ship.dy += 120.0*deltaTime
            }
            move(ship, width + 0.0, height + 0.0, deltaTime)
            move(asteroid, width + 0.0, height + 0.0, deltaTime)

            drawShip(ship, drawer)
            drawAsteroid(asteroid, drawer)

            drawer.fontMap = font
            drawer.fill = ColorRGBa.WHITE
            drawer.text("Asteroids™", width / 2.0, height / 2.0)
        }
    }
}

And when we run:

screen with ship and asteroid at same time

Now, clearly we’ll very soon put a type field into SpaceObject so that they’ll know how to draw themselves.

It seems like this code is going to be very compact. This is actually getting interesting sooner than I expected.

Commit: Added a ship, refactored for common code.

Watch this space!