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


There’s supposed to be a graphical effect when the ship explodes. Let’s produce one.

In the original arcade game, the explosion is a few line segments drifting away from the point where the ship was destroyed. In my other two Kotlin versions, there is the Splat, an object that displays a bunch of dots simulating an explosion. The View looks like this in the centralized version, not the current flat version:

class SplatView(lifetime: Double) {
    private val rot = Random.nextDouble(0.0, 360.0)
    private val ratio = U.DRAW_SCALE/30.0
    private var sizeTween = Tween(20.0*ratio, 100.0*ratio, lifetime)
    private var radiusTween = Tween(30.0*ratio, 5.0*ratio, lifetime)
    private val points = listOf(
        Point(-2.0, 0.0), Point(-2.0, -2.0), Point(2.0, -2.0),
        Point(3.0, 1.0), Point(2.0, -1.0), Point(0.0, 2.0),
        Point(1.0, 3.0), Point(-1.0, 3.0), Point(-4.0, -1.0),
        Point(-3.0, 1.0)
    )

    fun draw(splat: Splat, drawer: Drawer) {
        drawer.stroke = splat.color
        drawer.fill = splat.color
        drawer.rotate(rot)
        drawer.scale(splat.scale, splat.scale)
        for (point in points) {
            val size = sizeTween.value(splat.elapsedTime)
            val radius = radiusTween.value(splat.elapsedTime)
            drawer.circle(size*point.x, size*point.y, radius)
        }
    }
}

As you can see, this object uses two Tween2 objects, which automatically adjust their values between their two parameters, as time passes, so that sizeTween increases from 20 to 100 (times ratio, a scaling factor) and radiusTween declines from 30 down to 5. So the overall size of the explosion increases, and the size of the circles drawn goes down. The effect is of the explosion expanding and fading out.

splats appear as things explode

Now I think the spirit of this article series is to replicate the behavior of the other two versions, centralized and decentralized, in this “flat” version. So, even though I am tempted to do something different, I guess I need to replicate the same behavior here.

And the rules are that I have to do it without object methods.

I am tempted to import the object and then rip out its guts and make them into free-standing functions. But that wouldn’t give me the experience of trying to build this thing in the flat style.

So, while we will freely steal the ideas of Splat, let’s try to implement it more directly.

And let’s see whether TDD can help us at all. (Spoiler: I can’t come up with even one test. No biscuit.)

Tentative Plan

We’ll create a new Space Object, Splat. We’ll put one in the table of objects, marked inactive. When it’s time for the explosion (I think we’ll just do one for the ship (no, wait, surely we should do one for the saucer too)), we’ll prime it, activate it, and it’ll do its thing via a Timer.

I think we should to begin with something simple. Let’s start by having the Splat draw an expanding circle, starting at some small size, like the size of the ship, and ending up a few times larger. That will let us get a sense of the scale.

Let’s also start it out active and let it just repeatedly draw itself, somehow, so that we can watch on the screen as the object evolves.

Down To It

I’ll just add to the enum and keep on going.

enum class SpaceObjectType(
    val killRadius: (SpaceObject) -> Double,
    val draw: (Drawer, SpaceObject, Double) -> Unit
) {
    ASTEROID(asteroidRadius, drawAsteroid),
    SHIP(shipRadius, drawShip),
    SAUCER(saucerRadius, drawSaucer),
    MISSILE(missileRadius, drawShipMissile),
    SAUCER_MISSILE(missileRadius, drawSaucerMissile),
    SPLAT(splatRadius, drawSplat)
}

This demands the radius (kill radius, though splats will not collide) and the drawing function, splatRadius and drawSplat.

val asteroidRadius = { asteroid: SpaceObject -> U.AsteroidKillRadius* asteroid.scale}
val missileRadius = { _: SpaceObject -> U.MissileKillRadius }
val saucerRadius = { _: SpaceObject -> U.SaucerKilLRadius }
val shipRadius = { _: SpaceObject -> U.ShipKillRadius }
val splatRadius = { _: SpaceObject -> 0.0}

As a sort of spike, I try this:

var splatSize: Double = 10.0
val drawSplat = { drawer: Drawer, spaceObject: SpaceObject, deltaTime: Double ->
    splatSize += deltaTime
    if (splatSize > 100.0) {
        splatSize = 10.0
    }
    drawer.circle(Vector2.ZERO, splatSize)
}

Now I think that if we were to add a splat and set it active, it might draw circles.

fun newSplat(): SpaceObject = SpaceObject(
    type = SpaceObjectType.SPLAT,
    x = U.ScreenWidth * 0.75,
    y = U.ScreenHeight * 0.75,
    dx = 0.0,
    dy = 0.0,
    angle = 0.0,
    active = true
)

I added the argument names for the reader’s benefit. They show up in IDEA most of the time anyway.

I think now that if I add one, I should get a pulsating circle near the center.

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())
    objects.add(newSplat())
    SpaceObjects = objects.toTypedArray()
}

Let’s see what happens. Oops. A few tweaks are needed before I make the movie for you.

var splatSize: Double = 5.0
val drawSplat = { drawer: Drawer, spaceObject: SpaceObject, deltaTime: Double ->
    splatSize += deltaTime*5.0
    if (splatSize > 20.0) {
        splatSize = 5.0
    }
    drawer.fill = null
    drawer.circle(Vector2.ZERO, splatSize)
}

And we get this:

expanding circle repeats

Let’s reflect on what we’ve learned.

Reflection.

I had originally just added deltaTime to the size, and the size ranged from 10 to 100. That was too slow and too large. I think it’s closer now but still too large. It’s perhaps a bit slow but I’m not sure. We’ll want to experiment further.

I have in general been doing smooth timing things by having a value that represents the desired change in one second, which I then scale by deltaTime. I’m never clear on that, I find it hard to think about. We want smooth timing, so multiplying a standard effect size times delta time will apply just a bit when deltaTime is small, more when it’s larger.

I don’t see anything yet that I want to test. I am a bit sad about this, because I know that I do better when I’ve got tests, but I’m just not seeing it.

Let’s just keep going.

Proceeding

Let’s import that list of dot positions and draw those instead of the circle.

val splatPoints = listOf(
    Vector2(-2.0, 0.0), Vector2(-2.0, -2.0), Vector2(2.0, -2.0),
    Vector2(3.0, 1.0), Vector2(2.0, -1.0), Vector2(0.0, 2.0),
    Vector2(1.0, 3.0), Vector2(-1.0, 3.0), Vector2(-4.0, -1.0),
    Vector2(-3.0, 1.0)
)
var splatSize: Double = 5.0
val drawSplat = { drawer: Drawer, spaceObject: SpaceObject, deltaTime: Double ->
    splatSize += deltaTime*5.0
    if (splatSize > 20.0) {
        splatSize = 5.0
    }
    drawer.scale(splatSize, splatSize)
    for (point in splatPoints) {
        drawer.circle(point, 1/splatSize)
    }
}

 dot splats

That’s nearly good. I think it should start much smaller and should grow to not much larger than the starting size there. And it should be timed to about two seconds, and the dots should fade out. Let’s have a single timing variable, running from zero to one, controlling everything.

I think I’d like to commit this, as a save point. Commit: splat experiment.

I tweak a bit to get this:

smaller dots

Here’s the code for that, much like before, with different numbers:

var splatSize: Double = 1.0
val drawSplat = { drawer: Drawer, spaceObject: SpaceObject, deltaTime: Double ->
    splatSize += deltaTime*1.0
    if (splatSize > 5.0) {
        splatSize = 1.0
    }
    
    drawer.scale(splatSize, splatSize)
    for (point in splatPoints) {
        drawer.circle(point, 1/(5*splatSize))
    }
}

Thinking

OK, let’s see if I can settle on an approximate plan.

  • Two seconds of explosion seems about right.
  • Overall radius ranging from 1 to 5 seems about right.
  • Fading from opaque to zero might be a good idea.
  • We could probably avoid having a timer and deactivate right here.

So let’s have an elapsedTime ranging from 0 to 2, which we’ll increment by deltaTime resulting in 2 seconds.

When elapsedTime is 0, we want radius 1, when it’s 2 we want radius 5. So … radius is 1.0 + elapsedTime\*2.0

When elapsedTime is 0 we want opacity 1, when it’s 2 we want opacity 0. So … opacity is 1\*(2-elapsedTime). Right?

I always find it hard to figure these things out. That’s why I use tweens, even if I have to take a few weeks to implement them. (Haha, they only take minutes. More than one minutes.)

Let’s try this:

var elapsedSplatTime: Double = 0.0
val drawSplat = { drawer: Drawer, spaceObject: SpaceObject, deltaTime: Double ->
    elapsedSplatTime += deltaTime*1.0
    if (elapsedSplatTime > 2.0) {
        elapsedSplatTime = 0.0
    }
    var splatRadius = 1.0 + elapsedSplatTime*2.0
    drawer.scale(splatRadius, splatRadius)
    val opacity = 1.0*(2.0- elapsedSplatTime)
    val color = ColorRGBa(1.0 ,1.0, 1.0, opacity)
    drawer.fill = color
    drawer.stroke = color
    for (point in splatPoints) {
        drawer.circle(point, 1/(5*splatRadius))
    }
}

splat fading

I think that looks nearly good.

Let’s change that to deactivate itself instead of resetting its elapsed time, remove the active setting when we create it, save it in a convenient named variable, and trigger it when we deactivate the ship.

var elapsedSplatTime: Double = 0.0
val drawSplat = { drawer: Drawer, spaceObject: SpaceObject, deltaTime: Double ->
    elapsedSplatTime += deltaTime*1.0
    if (elapsedSplatTime > 2.0) {
        spaceObject.active = false
    }

fun newSplat(): SpaceObject = SpaceObject(
    type = SpaceObjectType.SPLAT,
    x = U.ScreenWidth * 0.75,
    y = U.ScreenHeight * 0.75,
    dx = 0.0,
    dy = 0.0,
    angle = 0.0,
    active = false
)

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())
    Splat = newSplat()
    objects.add(Splat)
    SpaceObjects = objects.toTypedArray()
}

And in deactivate we’ll have to check for the ship …

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

I expect this to work as intended.

ship splats on colliding with asteroid

That looks pretty good. I find that the missiles look a bit on the large side, might want to adjust that. But Bill likes them.

I could imagine these dots a bit larger. A fancy trick would be to change color, perhaps from yellow fading to red like flames. Beyond our scope today.

Let’s commit: Ship explodes with a Splat when destroyed. And sum up:

Summary

The bad news, surely3, is that I wrote no tests whatsoever for the Splat. I just couldn’t think of any. Perhaps it would be useful to implement some equivalent to the tween object, and to TDD the changing of the values as time elapses, but I didn’t even do that.

A test like that would at least document for me how to do this thing that I have to reinvent every so often, if I could find the test.

I’m not sure what to think about avoiding the use of a timer by having the Splat just set itself inactive. In the spirit of a compact, flat version of the program, it’s probably a pretty good idea. In the E-C-S context, if you’re going to do a timing thing, you should probably use the Timer.

Deactivating the asteroid required special code. That’s no surprise, and because we don’t allow ourselves real objects with methods, that code has to be patched into a generalized deactivate function. Messy, but it’s what you buy when you go to just using functions and not methods. It’s part of the “charm” of this design.

Personally, I don’t like this design as much as I might. I am tempted, as my next trick, to refactor this whole thing into a design that I consider to be decent. But I’m tired, so tired, of Asteroids. This program is making my Asteroids tired.

The code we have could use a little refactoring, and we should probably add a second Splat to serve as the explosion for the ship.

There is another possibility. In principle, there could be a Splat for every collision, so that there’d be a little splat when you split an asteroid4. If we were to do that, we might not want a big collection of Splat objects. Instead we might tuck the splat time into the basic object and have a flag for exploding status. The main thing to recommend that idea would be that it would be really messy.

Another possibility would be to create Splats as needed and destroy them when they’re done instead of the current fixed allocation. We’ll see. Might do it just because it’ll be a bit difficult.

Bottom line for today, the code needs a bit of improvement, but we have a quite decent explosion effect for the Ship. We need one for the Saucer, at least, and we’ll consider making little ones when we split an asteroid.

I’m pleased with the look of the result and not entirely happy with the state of the code. We’ll review it next time.

See you then!



  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. A “tween” is an object, function, or drawing that transitions between one state and another. In the early days of animation, the “real” artist would draw key poses, and then junior artists would draw the intermediate states between the key frames. In software, at least chez Ron5, we use the term for any mechanism that adjusts values between two states over time. 

  3. Don’t call me Shirley. Don’t call me at all. Tweet, toot, email, those are OK. 

  4. Split Splat. Nice. 

  5. There he goes again with the French.