GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

Rather than clean up the code more, I intend to bash in asteroid splitting. This is not wise but should be amusing.

When we hit an asteroid with a missile, the missile is deactivated, according to this code:

private fun checkCollisions() {
    val firstMissile = 0
    val lastMissile = 5
    val firstAsteroid = 7
    val lastAsteroid = spaceObjects.size - 1
    checkAllMissiles(firstMissile, lastMissile, firstAsteroid, lastAsteroid)
}

private fun checkAllMissiles(
    firstMissile: Int,
    lastMissile: Int,
    firstAsteroid: Int,
    lastAsteroid: Int
) {
    for (missile in spaceObjects.slice(firstMissile..lastMissile)
        .filter { it.active }) {
        val missilePos = Vector2(missile.x, missile.y)
        checkAllAsteroids(firstAsteroid, lastAsteroid, missilePos, missile)
    }
}

private fun checkAllAsteroids(
    firstAsteroid: Int,
    lastAsteroid: Int,
    missilePos: Vector2,
    missile: SpaceObject
) {
    for (asteroid in spaceObjects.slice(firstAsteroid..lastAsteroid)
        .filter { it.active }) {
        checkOneAsteroid(asteroid, missilePos, missile)
    }
}

private fun checkOneAsteroid(
    asteroid: SpaceObject,
    missilePos: Vector2,
    missile: SpaceObject
) {
    val asteroidPos = Vector2(asteroid.x, asteroid.y)
    val killDist = 16.0 * asteroid.scale + 1
    val dist = missilePos.distanceTo(asteroidPos)
    if (dist <= killDist) {
        deactivate(asteroid)
        deactivate(missile)
    }
}

I forgot to commit that this morning. Commit: refactor checkCollisions.

Now down there in the deactivate code, we would like to spawn two split asteroids, unless the current one is of size 1, and if there are two available. We want to spawn the new ones where the one we just killed was, and we’d like to have them receive random velocity.

(I suppose we could “simplify” this by resizing the one and spawning one more. That might actually be better, since we do need some values from the one we just shot down. Let’s go that way.)

private fun checkOneAsteroid(
    asteroid: SpaceObject,
    missilePos: Vector2,
    missile: SpaceObject
) {
    val asteroidPos = Vector2(asteroid.x, asteroid.y)
    val killDist = 16.0 * asteroid.scale + 1
    val dist = missilePos.distanceTo(asteroidPos)
    if (dist <= killDist) {
        if ( asteroid.scale > 1 ) {
            asteroid.scale /= 2
            setVelocity(asteroid, randomVelocity())
        } else deactivate(asteroid)
        deactivate(missile)
    }
}

This should halve the size of the shot asteroid and send it off in a random direction. It does exactly that. Now we just have to create a new one. I think I’ll do it this way:

private fun checkOneAsteroid(
    asteroid: SpaceObject,
    missilePos: Vector2,
    missile: SpaceObject
) {
    val asteroidPos = Vector2(asteroid.x, asteroid.y)
    val killDist = 16.0 * asteroid.scale + 1
    val dist = missilePos.distanceTo(asteroidPos)
    if (dist <= killDist) {
        if ( asteroid.scale > 1 ) {
            asteroid.scale /= 2
            setVelocity(asteroid, randomVelocity())
            spawnNewAsteroid(asteroid)
        } else deactivate(asteroid)
        deactivate(missile)
    }
}

Programming “by intention”, I declare that I want to spawn a new asteroid based on this one. Now all I have to do is code that up:

private fun spawnNewAsteroid(asteroid: SpaceObject) {
    val newOne: SpaceObject? = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID && ! it.active}.first()
    if (newOne != null ) {
        newOne.x = asteroid.x
        newOne.y = asteroid.y
        setVelocity(newOne, randomVelocity())
    }
}

I think that’ll do the job. It’s not lovely but it may serve.

I forgot to activate.

I also forgot to set the scale.

private fun spawnNewAsteroid(asteroid: SpaceObject) {
    val newOne: SpaceObject? = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID && ! it.active}.first()
    if (newOne != null ) {
        newOne.x = asteroid.x
        newOne.y = asteroid.y
        newOne.scale = asteroid.scale
        newOne.active = true
        setVelocity(newOne, randomVelocity())
    }
}

Ladies, gentlemen, and other kind folx, we have ourselves a game:

shooting and splitting asteroids

The good news with this game is that the ship can’t die. The bad news is that you don’t accumulate a score. But you can bang away all you want until the asteroids are all gone, then you’re alone in the silence of outer space.

I think there’s a bug here. Yes. Set velocity doesn’t mean what I thought it meant:

private fun setVelocity(spaceObject: SpaceObject, velocity: Vector2) {
    spaceObject.dx = velocity.x + Ship.dx
    spaceObject.dy = velocity.y + Ship.dy
}

I didn’t notice, because I wasn’t moving, but we’re adding the ship’s velocity to the asteroid. Not good.

We need a new method and the current setVelocity should be renamed to something like setVelocityRelativetoShip.

First the rename. Then find the senders and require a vanilla setVelocity, and in fact we’ll use it in the relative version:

private fun setVelocityRelativeToShip(spaceObject: SpaceObject, velocity: Vector2) {
    setVelocity(spaceObject, velocity + Vector2(Ship.dx, Ship.dy))
}

private fun setVelocity(spaceObject: SpaceObject, velocity: Vector2) {
    spaceObject.dx = velocity.x
    spaceObject.dy = velocity.y
}

fun activateAsteroid() {
    val asteroids = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID }
    val available = asteroids.firstOrNull { ! it.active }
    if (available != null) {
        available.active = true
        available.y = Random.nextDouble(U.ScreenHeight.toDouble())
        setVelocity(available, randomVelocity())
    }
}

private fun spawnNewAsteroid(asteroid: SpaceObject) {
    val newOne: SpaceObject? = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID && ! it.active}.first()
    if (newOne != null ) {
        newOne.x = asteroid.x
        newOne.y = asteroid.y
        newOne.scale = asteroid.scale
        newOne.active = true
        setVelocity(newOne, randomVelocity())
    }
}

That should fix that. Commit: fix defect where asteroids got ship velocity added in.

I get an interesting warning, that newOne is always non-null type. That’s here:

private fun spawnNewAsteroid(asteroid: SpaceObject) {
    val newOne: SpaceObject? = spaceObjects.filter 
    	{ it.type == SpaceObjectType.ASTEROID && ! it.active}
    	.first()
    if (newOne != null ) {
        newOne.x = asteroid.x
        newOne.y = asteroid.y
        newOne.scale = asteroid.scale
        newOne.active = true
        setVelocity(newOne, randomVelocity())
    }
}

My bug. first() will hurl if nothing is found. I meant firstOrNull. Thanks for the warning. Fix. Commit: fix defect that would have thrown if there were no asteroids available to split.

Let’s sum up before we do any more damage.

Summary

I rushed this, and I was distracted as well. I think the code likely works now, but there are no tests for it and I made at least three mistakes along the way, including one that I was ashamed to mention. OK, I’ll tell. When I created the setVelocity function, I copied and pasted the lines from the old version … including the addition of the Ship value. So everything went flying off at ship speed. Missiles got double ship speed added in and went sideways. Really ugly. I committed it without even testing in game, so it was even worse.

I am ashamed of myself. But I have been giving the best I have available this afternoon: it’s just not a very good best.

We now have a bit of a mess in the creation of objects. Here’s some code to demonstrate that:

private fun checkOneAsteroid(
    asteroid: SpaceObject,
    missilePos: Vector2,
    missile: SpaceObject
) {
    val asteroidPos = Vector2(asteroid.x, asteroid.y)
    val killDist = 16.0 * asteroid.scale + 1
    val dist = missilePos.distanceTo(asteroidPos)
    if (dist <= killDist) {
        if ( asteroid.scale > 1 ) {
            asteroid.scale /= 2
            setVelocityRelativeToShip(asteroid, randomVelocity())
            spawnNewAsteroid(asteroid)
        } else deactivate(asteroid)
        deactivate(missile)
    }
}

private fun spawnNewAsteroid(asteroid: SpaceObject) {
    val newOne: SpaceObject? = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID && ! it.active}.firstOrNull()
    if (newOne != null ) {
        newOne.x = asteroid.x
        newOne.y = asteroid.y
        newOne.scale = asteroid.scale
        newOne.active = true
        setVelocity(newOne, randomVelocity())
    }
}

private fun activateAsteroids(asteroidCount: Int) {
    deactivateAsteroids()
    for (i in 1..asteroidCount) {
        activateAsteroid()
    }
}

private fun deactivateAsteroids() {
    spaceObjects.filter { it.type == SpaceObjectType.ASTEROID }.forEach { deactivate(it) }
}

fun activateAsteroid() {
    val asteroids = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID }
    val available = asteroids.firstOrNull { ! it.active }
    if (available != null) {
        available.active = true
        available.y = Random.nextDouble(U.ScreenHeight.toDouble())
        setVelocity(available, randomVelocity())
    }
}

private fun randomVelocity(): Vector2 {
    val randomAngle = Random.nextDouble(360.0)
    return Vector2(U.AsteroidSpeed, 0.0).rotate(randomAngle)
}

private fun newMissile(): SpaceObject {
    return SpaceObject(SpaceObjectType.MISSILE, 0.0, 0.0, 0.0, 0.0, 0.0, false)
        .also { addComponent(it, Timer(it, 3.0)) }
}

private fun newShip(): SpaceObject
    = SpaceObject(SpaceObjectType.SHIP, 0.0, 0.0, 0.0, 0.0, 0.0, false)

private fun newAsteroid(): SpaceObject
        = SpaceObject(SpaceObjectType.ASTEROID, 0.0, 0.0, 0.0, 0.0, 0.0, false)
            .also { it.scale = 4.0}

Too many cooks. Duplication, partial duplication, dogs and cats living together, mass hysteria. And the naming: randomVelocity should be randomAsteroidVelocity, for example.

We need to sort this out and without objects to hang things on, my juggling ability is sore tested. We’ll work on that tomorrow. For now, we actually do have asteroids being shot down and split. This is a good thing, even if this is not code I can be proud of.

I forgive myself, of course, because I was doing the best I could do in the moment. We always do, you know.

See you next time!