GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo
See footnote:1


Let’s give a splat to the Saucer and maybe to splits.

Pleased with the Splat when the Ship is destroyed, I want the positive feeling that I’ll get when I manage to shoot down the Saucer. And, if time and brain permit, I’d like to have little Splats when I hit an Asteroid. Is that so much to ask?

Let’s begin with a review of the existing Splat logic. I suspect it needs a bit of improvement.

When the ship is deactivated, we do this:

fun deactivate(entity: SpaceObject) {
    if (splittable(entity)) {
        activateAsteroid(entity, entity.scale / 2, entity.position, randomVelocity())
        spawnNewAsteroid(entity)
    } else {
        entity.active = false
        resetComponents(entity)
    }
    if (entity.type == SpaceObjectType.SHIP) {
        Splat.active = true
        elapsedSplatTime = 0.0
        Splat.position = entity.position
        Splat.angle = randomAngle()
    }
}

That works because the only way the ship is ever deactivated is when it is shot down. For the Saucer, we may find the situation to be different. We might want to extract a function to do the Splat activation. It will express what’s going on and it might come in handy if we pass the Splat as a parameter.

fun deactivate(entity: SpaceObject) {
    if (splittable(entity)) {
        activateAsteroid(entity, entity.scale / 2, entity.position, randomVelocity())
        spawnNewAsteroid(entity)
    } else {
        entity.active = false
        resetComponents(entity)
    }
    if (entity.type == SpaceObjectType.SHIP) {
        activateSplat(entity,Splat)
    }
}

private fun activateSplat(entity: SpaceObject, splat: SpaceObject) {
    splat.active = true
    elapsedSplatTime = 0.0
    splat.position = entity.position
    splat.angle = randomAngle()
}

That took these steps:

  1. Extract Function: IDEA just gave it the entity parameter;
  2. Change Signature: give the function the splat parameter;
  3. Change Splat to splat in the function;
  4. Add the Splat parameter in the call.

Perhaps there was a better way to do that. Test. Commit: refactor out activateSplat function.

Let’s rename the Splat to ShipSplat, and create a new SaucerSplat.

fun createGame(saucerMissileCount: Int, shipMissileCount: Int, asteroidCount: Int) {
    Score = 0
    val objects = mutableListOf<SpaceObject>()
    for (i in 1..saucerMissileCount) objects.add(newSaucerMissile())
    for (i in 1..shipMissileCount) objects.add(newMissile())
    Ship = newShip()
    objects.add(Ship)
    Saucer = newSaucer()
    objects.add(Saucer)
    for (i in 1..asteroidCount) objects.add(newAsteroid())
    ShipSplat = newSplat()
    objects.add(ShipSplat)
    SaucerSplat = newSplat()
    objects.add(SaucerSplat)
    SpaceObjects = objects.toTypedArray()
}

Now when we shoot down the saucer, let’s activate the SaucerSplat.

We’re going to have a bit of trouble. Here’s how we check the collision:

private fun checkAllMissilesVsSaucer(saucer: SpaceObject) {
    for (missile: SpaceObject in activeShipMissiles(SpaceObjects)) {
        checkCollisionWithScore(saucer, missile, U.SaucerScore)
    }
}

fun checkCollisionWithScore(first: SpaceObject, second: SpaceObject, score: Int) {
    if (colliding(first, second)) {
        Score += score
        deactivate(first)
        deactivate(second)
    }
}

I don’t think we dare put the code in deactivate because I’m afraid that the Saucer gets deactivated by its timer. But does it? We’re in luck! It doesn’t call deactivate, it just sets the active flag:

fun update(component: Component, deltaTime: Double) {
    when (component) {
        is SaucerTimer -> {
            updateSaucerTimer(component, deltaTime)
        }
    }
}

fun updateSaucerTimer(timer: SaucerTimer, deltaTime: Double) {
    with(timer) {
        time -= deltaTime
        if (time <= 0.0) {
            time = U.SaucerDelay
            if (entity.active) {
                entity.active = false
            } else {
                activateSaucer(entity)
            }
        }
    }
}

So in deactivate:

fun deactivate(entity: SpaceObject) {
    if (splittable(entity)) {
        activateAsteroid(entity, entity.scale / 2, entity.position, randomVelocity())
        spawnNewAsteroid(entity)
    } else {
        entity.active = false
        resetComponents(entity)
    }
    if (entity.type == SpaceObjectType.SHIP) {
        activateSplat(entity,ShipSplat)
    }
    if (entity.type == SpaceObjectType.SAUCER) {
        activateSplat(entity,SaucerSplat)
    }
}

I think this works. It does. Commit: Saucer explodes with Splat when hit by missile.

Reflection

The bad news is that I’ve tested the Splats in the game, but have no automated tests for them. We do have some collision tests. Let’s see if we can devise a simple test or two for the Splats, now that we know how they work.

    @Test
    fun `missile killing saucer activates splat`() {
        createGame(4,4,23)
        assertThat(SaucerSplat.active).isEqualTo(false)
        val missile = newMissile()
        Saucer.active = true
        missile.position = Saucer.position
        checkCollisionWithScore(Saucer, missile, 222)
        assertThat(Saucer.active).isEqualTo(false)
        assertThat(SaucerSplat.active).isEqualTo(true)
    }

That runs green. I think I could have written that in advance. I just didn’t have the idea clearly enough, or I was impatient.

Let’s do similarly for the Ship.

    @Test
    fun `missile killing ship activates splat`() {
        createGame(4,4,23)
        assertThat(ShipSplat.active).isEqualTo(false)
        val missile = newSaucerMissile()
        Ship.active = true
        missile.position = Ship.position
        checkCollisionWithScore(Ship, missile, 222)
        assertThat(Ship.active).isEqualTo(false)
        assertThat(ShipSplat.active).isEqualTo(true)
    }

OK, I feel better now. Commit: add tests for activating ship and saucer splats.

Reflection

OK, we have the saucer making a nice happy splat when we hit it and the ship making a sad splat2 when it hits an asteroid or is shot down by the saucer.

Let’s think about how we might approach splats for when we hit an asteroid.

In fact, let’s do a Spike:

Spike

I have an idea. Here’s where we split an asteroid:

fun deactivate(entity: SpaceObject) {
    if (splittable(entity)) {
        activateAsteroid(entity, entity.scale / 2, entity.position, randomVelocity())
        spawnNewAsteroid(entity)
    } else {
        entity.active = false
        resetComponents(entity)
    }
    if (entity.type == SpaceObjectType.SHIP) {
        activateSplat(entity,ShipSplat)
    }
    if (entity.type == SpaceObjectType.SAUCER) {
        activateSplat(entity,SaucerSplat)
    }
}

What if we pasted in an activateSplat up there?

fun deactivate(entity: SpaceObject) {
    if (splittable(entity)) {
        activateSplat(entity, SaucerSplat)
        activateAsteroid(entity, entity.scale / 2, entity.position, randomVelocity())
        spawnNewAsteroid(entity)
    } else {
        entity.active = false
        resetComponents(entity)
    }
    if (entity.type == SpaceObjectType.SHIP) {
        activateSplat(entity,ShipSplat)
    }
    if (entity.type == SpaceObjectType.SAUCER) {
        activateSplat(entity,SaucerSplat)
    }
}

Won’t that just work, with a massive splat when we hit an asteroid?

Sure enough, it does. Well, almost. It doesn’t splat the smallest asteroids, because they’re not splittable. It’s nearly that easy, except that we should probably have more splats to use.

Let’s do it like the others, still using the SaucerSplat:

fun deactivate(entity: SpaceObject) {
    if (splittable(entity)) {
        activateAsteroid(entity, entity.scale / 2, entity.position, randomVelocity())
        spawnNewAsteroid(entity)
    } else {
        entity.active = false
        resetComponents(entity)
    }
    if (entity.type == SpaceObjectType.SHIP) {
        activateSplat(entity,ShipSplat)
    }
    if (entity.type == SpaceObjectType.SAUCER) {
        activateSplat(entity,SaucerSplat)
    }
    if (entity.type == SpaceObjectType.ASTEROID) {
        activateSplat(entity,SaucerSplat)
    }
}

Now let me check the baby asteroids. Yes, they splat now. I notice in playing that even though we are reusing the SaucerSplat, you don’t really notice what happens when you hit two things rapidly. In reality the one splat immediately stops and a new one starts at the new hit. But the eye doesn’t see that happening. It looks perfectly reasonable.

We could nonetheless add a third Splat for asteroids. Let’s do that, it’s almost free.

We’re not spiking.

I’m calling this good and going for production.

var Score: Int = 0
var AsteroidsGoneFor = 0.0
private var currentWaveSize = 0
var dropScale = U.ShipDropInScale
lateinit var Saucer: SpaceObject
var saucerSpeed = U.SaucerSpeed
lateinit var Ship: SpaceObject
lateinit var SpaceObjects: Array<SpaceObject>
lateinit var ShipSplat: SpaceObject
lateinit var SaucerSplat: SpaceObject
lateinit var AsteroidSplat: SpaceObject // new
var TimerTable: List<Timer> = mutableListOf<Timer>()


fun createGame(saucerMissileCount: Int, shipMissileCount: Int, asteroidCount: Int) {
    Score = 0
    val objects = mutableListOf<SpaceObject>()
    for (i in 1..saucerMissileCount) objects.add(newSaucerMissile())
    for (i in 1..shipMissileCount) objects.add(newMissile())
    Ship = newShip()
    objects.add(Ship)
    Saucer = newSaucer()
    objects.add(Saucer)
    for (i in 1..asteroidCount) objects.add(newAsteroid())
    ShipSplat = newSplat()
    objects.add(ShipSplat)
    SaucerSplat = newSplat()
    objects.add(SaucerSplat)
    AsteroidSplat = newSplat()
    objects.add(AsteroidSplat)
    SpaceObjects = objects.toTypedArray()
}

fun deactivate(entity: SpaceObject) {
    if (splittable(entity)) {
        activateAsteroid(entity, entity.scale / 2, entity.position, randomVelocity())
        spawnNewAsteroid(entity)
    } else {
        entity.active = false
        resetComponents(entity)
    }
    if (entity.type == SpaceObjectType.SHIP) {
        activateSplat(entity,ShipSplat)
    }
    if (entity.type == SpaceObjectType.SAUCER) {
        activateSplat(entity,SaucerSplat)
    }
    if (entity.type == SpaceObjectType.ASTEROID) {
        activateSplat(entity,AsteroidSplat)
    }
}

And let’s add the test since we know how.

    @Test
    fun `missile killing asteroid activates splat`() {
        createGame(4,4,23)
        assertThat(AsteroidSplat.active).isEqualTo(false)
        val missile = newSaucerMissile()
        val asteroid = newAsteroid()
        missile.position = asteroid.position
        checkCollisionWithScore(asteroid, missile, 222)
        assertThat(AsteroidSplat.active).isEqualTo(true)
    }

Green. Commit: Asteroid hits share one Splat. Here’s a movie3.

lots of splats

Let’s sum up.

Summary

Two interesting things this morning.

First, testing the Splats turned out to be pretty easy and yet I didn’t see how to do it, or have the gumption to figure it out. One way or the other, I lost the bit of confidence that goes with tests running, both during development and then later when making other changes. All good now, we do have those tests, and they’ll provide a bit more safety.

Second, I was expecting to have to make a number of splats for the asteroids, so that rapid fire would look OK. But even with just one it looks good to me. If customers or the boss complain, it’ll be easy enough to provide more than one, but I’m pleased that it wasn’t necessary.

Third4, we’re seeing more and more type-checking code in here, such as in the deactivate above, which has three explicit type checks and the subordinate one in splittable. With explicit types and methods, instead of this constrained “functions only” design, they’re bound to turn up. If you were to review the original Asteroids assembly code, you’d find the same kind of checking and branching. When the language doesn’t help you, that’s what you wind up doing.

It’s not awful. There may be some way to make things a bit more compact. We’ll do some code review within the next couple of articles and see what we can conclude about our three different styles of building the same program.

For now, a good morning’s work. Split Splat all over. See you next time!



  1. The repos up at the top are three different designs, essentially the same program, done so that we can compare them. Unfortunately you may have to read around 300 articles to get the entire picture. I apologize for my prolixity. 

  2. He’s probably projecting. I don’t think the Splats care all that much either way. 

  3. I’ve had a suggestion to save movies in other than .mov format. If you’d prefer another format, tweet or toot me. Thanks! 

  4. Always happens when I say I have N things, I always come up with at least one more.