GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

It seems clear that this ‘flat’ no-objects version of the game wants to know certain key objects. Let’s see how we might deal with that.

Yesterday, when I created the initial gameCycle function, I left out the part about applying controls, leaving it in main’s extend, along with the creation of our two objects:

val ship = SpaceObject(
        SpaceObjectType.SHIP,
        512.0,
        512.0,
        100.0,
        -90.0,
        45.0,
    )
    val asteroid = SpaceObject(
        SpaceObjectType.ASTEROID,
        300.0,
        300.0,
        74.0,
        40.0,
    )
    val spaceObjects = arrayOf(asteroid, ship)

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

fun gameCycle(
    spaceObjects: Array<SpaceObject>,
    width: Int,
    height: Int,
    drawer: Drawer,
    deltaTime: Double
) {
    for (spaceObject in spaceObjects) {
        move(spaceObject, width, height, deltaTime)
    }
    for (spaceObject in spaceObjects) {
        draw(spaceObject, drawer)
    }
}

Now I have in mind that we’ll mostly process the space objects all the same, with any special treatment triggered by checking the values in their particular data record. But there is at least one case where the game cycle will really want to know which object is the ship: applying the controls.

Or is there? We can do that by checking the type as well. Oh my. This is a bit nasty.

Let me try this:

for (spaceObject in spaceObjects) {
    if (spaceObject.type == SpaceObjectType.SHIP) {
        if (controls_accelerate) {
            spaceObject.dy += 120.0*deltaTime
        }
    }
    move(spaceObject, width, height, deltaTime)
}
for (spaceObject in spaceObjects) {
    draw(spaceObject, drawer)
}

Sure, that’ll work just fine, won’t it? Of course, this isn’t the correct code for acceleration, but if I hold down the “j” key, the ship will accelerate downward. And it does. Commit: move control of ship into gameCycle.

As long as we’re here, let’s give the ship some more control, left, right, and acceleration in the direction it’s pointing.

I’ll steal the base code from another version, since I’ve worked out all the numbers. The code I’m borrowing from looks like this:

    private fun turn(obj: Ship, deltaTime: Double) {
        if (left) obj.turnBy(-U.SHIP_ROTATION_SPEED*deltaTime)
        if (right) obj.turnBy(U.SHIP_ROTATION_SPEED*deltaTime)
    }

    const val SHIP_ROTATION_SPEED = 200.0 // degrees per second

    fun turnBy(degrees: Double) {
        heading += degrees
    }

Naturally we won’t go through all that rigmarole here in the early days of a different no-OO design. Instead:

if (spaceObject.type == SpaceObjectType.SHIP) {
    if ( controls_left ) spaceObject.angle -= 250.0*deltaTime
    if ( controls_right ) spaceObject.angle += 250.0*deltaTime
    if (controls_accelerate) {
        spaceObject.dy += 120.0*deltaTime
    }
}

That should let me rotate the ship in play. And it does. Commit: ship rotates left on “d”, right on “f”.

Now to rotate the acceleration to match, I can borrow from this:

private fun accelerate(ship:Ship, deltaTime: Double) {
    if (accelerate) {
        val deltaV = U.SHIP_ACCELERATION.rotate(ship.heading) * deltaTime
        ship.accelerate(deltaV)
    }
}

val SHIP_ACCELERATION = Velocity(120.0, 0.0)

I emulate that:

for (spaceObject in spaceObjects) {
    if (spaceObject.type == SpaceObjectType.SHIP) {
        if ( controls_left ) spaceObject.angle -= 250.0*deltaTime
        if ( controls_right ) spaceObject.angle += 250.0*deltaTime
        if (controls_accelerate) {
            val deltaV = Vector2(120.0,0.0).rotate(spaceObject.angle) * deltaTime
            spaceObject.dx += deltaV.x
            spaceObject.dy += deltaV.y
        }
    }
    move(spaceObject, width, height, deltaTime)
}
for (spaceObject in spaceObjects) {
    draw(spaceObject, drawer)
}

And the ship flys just as one would want. Still drawn thickly and perhaps not to the desired scale.

Commit: ship turns and accelerates properly.

Reflection

We seem to be beginning to get code that may be close to what we might imagine would be similar to the sort of design that we might be inclined to be working toward. (Is that tentative enough?) So what do we see that is worth mentioning?

Since apparently I feel free to use the Vector2 built-in type, we should consider representing position, velocity, and acceleration as Vector2 rather than breaking out the x’s and y’s. We ought to make that change as early as possible, since there’ll be more code to change the longer we wait.

And we’ll want to do something about the magic numbers, creating some well-known constants.

Finally, we can and probably should extract those bits into little functions, like this:

Before:

if (spaceObject.type == SpaceObjectType.SHIP) {
    if ( controls_left ) spaceObject.angle -= 250.0*deltaTime
    if ( controls_right ) spaceObject.angle += 250.0*deltaTime
    if (controls_accelerate) {
        val deltaV = Vector2(120.0,0.0).rotate(spaceObject.angle) * deltaTime
        spaceObject.dx += deltaV.x
        spaceObject.dy += deltaV.y
    }
}

After:

if (spaceObject.type == SpaceObjectType.SHIP) {
    applyControls(spaceObject, deltaTime)
}

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) {
        val deltaV = Vector2(120.0, 0.0).rotate(spaceObject.angle) * deltaTime
        spaceObject.dx += deltaV.x
        spaceObject.dy += deltaV.y
    }
}

Commit: extract function for clarity.

Now we might ask ourselves why we should do that extraction. It makes the program just a tiny bit slower. I’d argue that it makes it a bit more clear, but it’s hard to argue that it makes a big difference. I’d say that “chunking” ideas into named batches is a good thing. Yes, even when we’re writing very procedural code.

From a larger viewpoint, the program is doing some good things. I can fly my ship around now. And all the interesting code sums up to only about 170 lines, broken out into three files:

  1. SpaceObject holds the record definition for space objects and moving.
  2. Game holds the gameCycle function and applyControls.
  3. TemplateKT holds the simple main program that runs the game cycle.

It’s early days, but I still feel that we’ll be able to have a decent, reasonably clear design without objects, and I suspect that the program will be more compac without much loss of clarity.

I hope all three of you will continue to follow along. See you next time!