Kotlin 289 - Split Splat
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:
- Extract Function: IDEA just gave it the entity parameter;
- Change Signature: give the function the splat parameter;
- Change
Splat
tosplat
in the function; - 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.
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!
-
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. ↩
-
He’s probably projecting. I don’t think the Splats care all that much either way. ↩
-
I’ve had a suggestion to save movies in other than
.mov
format. If you’d prefer another format, tweet or toot me. Thanks! ↩ -
Always happens when I say I have N things, I always come up with at least one more. ↩