GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

We’re keeping score in our head. Let’s display it. I fully intend to steal the code for this from another version. We also handle ship-asteroid collisions and provide respawn.

In the centralized version, here’s how we draw the score:

    private fun drawScore(drawer: Drawer) {
        drawer.isolated {
            translate(charSpace/2, lineSpace)
            stroke = ColorRGBa.GREEN
            fill = ColorRGBa.GREEN
            text(formatted(), Point(0.0, 0.0))
        }
    }

    fun formatted(): String = ("00000" + totalScore.toShort()).takeLast(5)

The interesting values include:

    private val lineSpace = U.FONT_SIZE
    private val charSpace = 30.0 // random guess seems good
    const val FONT_SIZE = 64.0

    program {
        val font = loadFont("data/fonts/default.otf", U.FONT_SIZE)

Did I leave the font loading in this version? I did. I guess the text method just magically knows the font because it’s set there in program. Let’s spike this in, since it’s pure display.

fun gameCycle(
    spaceObjects: Array<SpaceObject>,
    width: Int,
    height: Int,
    drawer: Drawer,
    deltaTime: Double
) {
    for (spaceObject in spaceObjects) {
        for (component in spaceObject.components) update(component, deltaTime)
        if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
        move(spaceObject, width, height, deltaTime)
    }
    for (spaceObject in spaceObjects) {
        if (spaceObject.active) draw(spaceObject, drawer)
    }
    checkCollisions()
    drawScore(drawer)
}

private fun drawScore(drawer: Drawer) {
    val charSpace = 30.0
    val lineSpace = 64.0
    drawer.isolated {
        translate(charSpace/2, lineSpace)
        stroke = ColorRGBa.GREEN
        fill = ColorRGBa.GREEN
        text(formatted(), Vector2(0.0, 0.0))
    }
}

fun formatted(): String = ("00000" + Score.toShort()).takeLast(5)

The score does display, but it is, um, rather smaller than I had in mind:

big game screen tiny score

Last time I tried to find out the current scale in the display, I failed. We’re using a general scale of 4 for things, and whatever scale we use, we apply it after translation, so let’s just fiddle it in.

private fun drawScore(drawer: Drawer) {
    val charSpace = 30.0
    val lineSpace = 64.0
    drawer.isolated {
        translate(charSpace/2, lineSpace)
        scale(4.0, 4.0)
        stroke = ColorRGBa.GREEN
        fill = ColorRGBa.GREEN
        text(formatted(), Vector2(0.0, 0.0))
    }
}

Oh I love it when a plan comes together!

big game perfect score display

Nice. Now we really have a game. You can even rack up a score. Let’s do a bit of reflection, though, there’s something really solid to talk about here.

Reflection

In this version, we have a global variable, Score, and there’s code in the collision logic to accumulate the score, and an explicit draw in the game cycle to draw it.

fun checkOneAsteroid(
    asteroid: SpaceObject,
    missile: SpaceObject
) {
    if (colliding(asteroid, missile)) {
        Score += getScore(asteroid)
        ...

In the centralized version, we have:

  1. A MissileCollisionStrategy checks for collision with an Asteroid, and
  2. If it collides, gets the score from the Missile, which
  3. Will be non-zero if the Missile is from the ship, and
  4. Adds it to a Transaction, which
  5. Passes it to the SpaceObjectCollection, which
  6. Passes it to the ScoreKeeper, which
  7. Is fetched and explicitly drawn by the game cycle.

The decentralized version is a bit differently complicated compared to this, because the ScoreKeeper is an object in space and it interacts automatically with Score objects, which are also floating about in space. I still like that design a lot. But I digress.

Now of course much of that could be bypassed, so that the collision strategy could get the score and stuff it in a global, and it could even ask the missile if it is from the ship and so we could cut out four or five of those steps, making it as simple as this is.

Boiling it down, in this version we have an asteroid dumping its score into a global, and a direct display of that global, compared to an interaction of at least six different objects. Each of the interactions tends to be just a line or two, but the action does jump all over to do the job. It’s almost certainly harder to understand.

Which is better? Which is simpler? What do we even mean by better, or simpler?

At the scale of this game, I’d forgive anyone who said that the way we did it today is better and simpler, although we are faced with a future story that will be interesting. Let’s speculate a bit. The story is this:

Saucer missiles do destroy asteroids, but do not accumulate score for the player.

In the centralized version, this is done trivially. When we create an asteroid, it is assumed not to be from the ship unless the ship creates it. And when the missile is asked to score an asteroid, it only adds in the score if it is from the ship. It’s all down to the missile.

To implement that here, we’ll first have to know whether missiles are from the ship or not. I was originally planning to do that based on their position in the object table, but we’ll clearly need a flag of some kind in the missile, or perhaps to do a lookup to find out where it came from. Then, somewhere, probably in checkOneAsteroid, we’ll need to look at the missile to decide what to do. The conditional logic there will need to be enhanced.

It’ll be similar, but we’ll have to find it, and we’ll be checking bits that don’t belong to us to decide. It’ll be just a bit more gritty, a bit more complex, in the small area around checking one asteroid.

When we code as we are coding on this version, we tend to get conditional logic tucked around in strange corners to make things happen. As we do the rest of the collision logic, we’ll see that happening.

Conclusion?

Not so much a conclusion as an observation. The code for scoring is unquestionably smaller and arguably simpler than the code in our other versions. Right now, it looks better. However, it may turn out to be less flexible and easy to change. We’ll find out as we move forward.

Next?

I think that what we should do next is to allow the ship to collide with asteroids, and be destroyed. There’s a lot involved in that simple statement:

  1. Colliding and destroying the ship (setting it inactive) should be easy;
  2. We need to provide more than one ship per quarter;
  3. We have no way to insert a quarter and start the game;
  4. We need to handle running out of ships, displaying GAME OVER;
  5. We need the “attract” screen that shows asteroids floating.

There’s probably more. We can start with checking the ship and asteroids for colliding, and setting the ship inactive.

I think we’ll spike this. Let’s commit the score before I forget. Commit: game keeps and displays score.

My tentative plan is to do the ship vs asteroids much like the missiles vs asteroids, and then look for duplication and code that can be combined.

private fun checkCollisions() {
    checkAllMissiles()
}

That becomes …

private fun checkCollisions() {
    checkAllMissilesVsAsteroids()
}

Just a rename for clarity. Commit: rename checkAllMissiles.

private fun checkCollisions() {
    checkAllMissilesVsAsteroids()
    checkShipVsAsteroids()
}

Now I have to implement that. Here goes, follow me down. I wind up with this:

private fun checkCollisions() {
    checkAllMissilesVsAsteroids()
    if ( Ship.active ) checkShipVsAsteroids(Ship)
}

private fun checkShipVsAsteroids(ship: SpaceObject) {
    for (asteroid in activeAsteroids(SpaceObjects)) {
        if (collidingShip(asteroid, ship)) {
            if (asteroid.scale > 1) {
                asteroid.scale /= 2
                asteroid.velocity = randomVelocity()
                spawnNewAsteroid(asteroid)
            } else deactivate(asteroid)
            deactivate(ship)
        }
    }
}

private fun collidingShip(asteroid: SpaceObject, ship: SpaceObject): Boolean {
    val asteroidSize = 16.0*asteroid.scale
    val shipSize = 12.0
    return asteroid.position.distanceTo(ship.position) < asteroidSize+shipSize
}

That works well, except that once you collide with an asteroid, you’ll never come back, so the game is intensely boring once that happens. You can’t even put in another quarter.

More Ships

Let’s create a new ship whenever the ship goes inactive. We’ll have a bit of a delay.

fun gameCycle(
    spaceObjects: Array<SpaceObject>,
    width: Int,
    height: Int,
    drawer: Drawer,
    deltaTime: Double
) {
    for (spaceObject in spaceObjects) {
        for (component in spaceObject.components) update(component, deltaTime)
        if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
        move(spaceObject, width, height, deltaTime)
    }
    for (spaceObject in spaceObjects) {
        if (spaceObject.active) draw(spaceObject, drawer)
    }
    checkCollisions()
    drawScore(drawer)
    checkIfShipNeeded()
}

That should do it except for some details:

fun gameCycle(
    spaceObjects: Array<SpaceObject>,
    width: Int,
    height: Int,
    drawer: Drawer,
    deltaTime: Double
) {
    for (spaceObject in spaceObjects) {
        for (component in spaceObject.components) update(component, deltaTime)
        if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
        move(spaceObject, width, height, deltaTime)
    }
    for (spaceObject in spaceObjects) {
        if (spaceObject.active) draw(spaceObject, drawer)
    }
    checkCollisions()
    drawScore(drawer)
    checkIfShipNeeded(deltaTime)
}

var ShipGoneFor = 0.0

fun checkIfShipNeeded(deltaTime: Double) {
    if ( ! Ship.active ) {
        ShipGoneFor += deltaTime
        if (ShipGoneFor > 4.0) {
            Ship.position = Vector2(512.0, 512.0)
            Ship.velocity = Vector2(0.0,0.0)
            Ship.active = true
            ShipGoneFor = 0.0
        }
    }
}

When the ship is gone, I count up to 4 seconds, then set the ship still, at center screen, and active, and reset the timer. Perfect. When the ship is destroyed, four seconds later it appears at center screen.

I forgot that we have literals for width and height of screen.

    Ship.position = Vector2(U.ScreenWidth/2.0, U.ScreenHeight/2.0)

Perfect. Commit: game creates new ships indefinitely.

We’re at 300 lines of article. Let’s sum up.

Summary

We’ve done three stories this morning.

Display Score
This was nearly trivial, just a display of the text value of the internal score value we implemented yesterday. We discussed that this seems much simpler than in our more object-oriented versions. It may or may not be less flexible, which remains to be seen.
Ship Collides with Asteroid
This was a straightforward copy-pasta exercise. We have fairly substantial duplication as things stand now, more than I want to display here. We’ll work on reducing that duplication, perhaps as soon as next time. A simple job but left a bit of a mess.
Spawn New Ships
We now detect that the Ship is inactive, and after four seconds, activate it again. It should be easy to put in a count of the number of ships we can spawn this way, and more closer to having a game that can make money from all the quarters we type into it. Cha ching!

All three of these stories went in pretty well, mostly consisting of inserting a call to a new function, and implementing that function. A fair amount of conditional logic, but that seems to be the nature of the beast.

No Tests
I did all that with no new tests. That’s not commendable but the score display is hardly able to be tested, and the timing logic for the ship could be tested but it’s not very interesting. The ship collision logic certainly could be tested, but I was just copying existing code, and didn’t.

I’m rationalizing. I’ve made the system more complicated, unquestionably, and I’ve not improved the tests. I’m confident in the new code but there’s no basis for anyone else to be confident in the absence of tests. And what about that refactoring I plan to do soon? How will I be sure I’ve not broken anything.

I think I’ll give this morning an A for new capability and a C or C- for quality of work. The code needs refactoring and there are no tests. Overall score, C+, maybe B-.

But we do have the new features. That’s worth something, isn’t it? Isn’t it?

This was what I had to give this morning. Next time, I’ll try to reclaim my honor. Join me, please.