Kotlin 214 - Interactions
GitHub Decentralized Repo
GitHub Centralized Repo
Let’s try replacing interactions all at one go. It might not be too difficult. In Post: I do it, it works, I roll it back.
The Game cycle
goes like this:
fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
val deltaTime = elapsedSeconds - lastTime
lastTime = elapsedSeconds
tick(deltaTime)
beforeInteractions()
processInteractions()
U.AsteroidTally = knownObjects.asteroidCount()
createNewWaveIfNeeded()
createSaucerIfNeeded()
createShipIfNeeded()
drawer?.let { draw(drawer) }
}
I’ll just replace the guts of processInteractions
and see what we can do.
fun processInteractions() = knownObjects.applyChanges(changesDueToInteractions())
Well, maybe not that … how about this:
fun changesDueToInteractions(): Transaction {
val trans = Transaction()
knownObjects.pairsToCheck().forEach { p ->
p.first.callOther(p.second, trans)
p.second.callOther(p.first, trans)
}
return trans
}
Right, that’s more like it. I’ll save that as a comment and do a new one.
I just typed this in:
fun changesDueToInteractions(): Transaction {
val trans = Transaction()
with (knownObjects) {
missiles.forEach { missile ->
ships.forEach { ship ->
missile.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithMissile(missile, trans)
}
saucers.forEach { saucer ->
missile.subscriptions.interactWithSaucer(saucer, trans)
saucer.subscriptions.interactWithMissile(missile, trans)
}
asteroids.forEach {asteroid ->
missile.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithMissile(missile, trans)
}
}
asteroids.forEach { asteroid ->
ships.forEach { ship ->
ship.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithShip(ship, trans)
}
saucers.forEach { saucer ->
saucer.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithSaucer(saucer, trans)
}
}
}
return trans
}
Missiles hit ships, saucers, and asteroids. Asteroids hit ships and saucers. That makes the game work just fine.
I expect the tests may run as well, but let’s find out. Oddly, they do not. Curiously, I find that three test files have been modified. I must have done a rename that ran wild. Roll the tests back. Test. They are green. I think we probably have tests for most of this, but do we have any that do processInteractions
or changesDueToInteractions
? We do. Let’s review them.
@Test
fun `create game`() {
val game = Game()
val asteroid = Asteroid(Vector2(100.0, 100.0), Vector2(50.0, 50.0))
val ship = Ship(
position = Vector2(1000.0, 1000.0)
)
game.add(asteroid)
game.add(ship)
val trans = game.changesDueToInteractions()
assertThat(trans.removes.size).isEqualTo(0)
val steps = (1000-100)/50
for (i in 1..steps * 60) game.tick(1.0 / 60.0)
val x = asteroid.position.x
val y = asteroid.position.y
assertThat(x).isEqualTo(100.0 + steps * 50.0, within(0.1))
assertThat(y).isEqualTo(100.0 + steps * 50.0, within(0.1))
val trans2 = game.changesDueToInteractions()
println(trans2.firstRemove())
assertThat(trans2.removes.size).isEqualTo(2)
}
@Test
fun `collision detection`() {
val game = Game()
val a = Asteroid(Vector2(100.0, 100.0))
game.add(a)
val s = Ship(
position = Vector2(100.0, 150.0)
)
game.add(s)
assertThat(game.knownObjects.size).isEqualTo(2)
val colliders = game.changesDueToInteractions()
assertThat(colliders.removes.size).isEqualTo(2)
}
@Test
fun `stringent colliders`() {
val p1 = Vector2(100.0, 100.0)
val p2 = Vector2(1250.0, 100.0)
val game = Game()
val a0 = Asteroid(p1) // yes
game.add(a0)
val m1 = Ship(position = p1, killRadius = 10.0) // yes
game.add(m1)
val s2 = Ship(
position = p1
) // yes kr=150
game.add(s2)
val a3 = Asteroid(p2) // no
game.add(a3)
val a4 = Asteroid(p2) // no
game.add(a4)
val colliders = game.changesDueToInteractions()
assertThat(colliders.removes.size).isEqualTo(3)
}
Well, those don’t cover all the cases, but they do give a bit of confidence. I think the code rather obviously works, and the game clearly runs. I’ve exercised all the possibilities except for me being hit by the saucer missile. I’ll be right back.
Curiously, the saucer is not firing any missiles. “The changes I made couldn’t have affected that.”
A quick print tells me that it’s not seeing the ship. Why not?
Quickly swapping the new code out tells me that … oh … we’re not allowing the ship and saucer to collide with each other! I didn’t cover all the cases after all. I update the method:
fun changesDueToInteractions(): Transaction {
val trans = Transaction()
with (knownObjects) {
missiles.forEach { missile ->
ships.forEach { ship ->
missile.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithMissile(missile, trans)
}
saucers.forEach { saucer ->
missile.subscriptions.interactWithSaucer(saucer, trans)
saucer.subscriptions.interactWithMissile(missile, trans)
}
asteroids.forEach {asteroid ->
missile.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithMissile(missile, trans)
}
}
asteroids.forEach { asteroid ->
ships.forEach { ship ->
ship.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithShip(ship, trans)
}
saucers.forEach { saucer ->
saucer.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithSaucer(saucer, trans)
}
}
ships.forEach { ship ->
saucers.forEach { saucer ->
saucer.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithSaucer(saucer, trans)
}
}
}
return trans
}
Let’s do some refactoring here. Extract some methods. IDEA is confused by that idea. I remove the with
, which I did by hand, and then we get this:
fun changesDueToInteractions(): Transaction {
val trans = Transaction()
missilesVsEverything(trans)
asteroidsVsShipsAndSaucers(trans)
shipsVsSaucers(trans)
return trans
}
private fun shipsVsSaucers(trans: Transaction) {
knownObjects.ships.forEach { ship ->
knownObjects.saucers.forEach { saucer ->
saucer.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithSaucer(saucer, trans)
}
}
}
private fun asteroidsVsShipsAndSaucers(trans: Transaction) {
knownObjects.asteroids.forEach { asteroid ->
knownObjects.ships.forEach { ship ->
ship.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithShip(ship, trans)
}
knownObjects.saucers.forEach { saucer ->
saucer.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithSaucer(saucer, trans)
}
}
}
private fun missilesVsEverything(trans: Transaction) {
knownObjects.missiles.forEach { missile ->
knownObjects.ships.forEach { ship ->
missile.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithMissile(missile, trans)
}
knownObjects.saucers.forEach { saucer ->
missile.subscriptions.interactWithSaucer(saucer, trans)
saucer.subscriptions.interactWithMissile(missile, trans)
}
knownObjects.asteroids.forEach { asteroid ->
missile.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithMissile(missile, trans)
}
}
}
I think we might want to put the with
back in. But let’s do some more extracts here, see if we like it:
IDEA seems not to get the drift of what I want. I’m doing it manually in most cases. I’ve got this much:
fun changesDueToInteractions(): Transaction {
val trans = Transaction()
missilesVsEverything(trans)
asteroidsVsShipsAndSaucers(trans)
shipsVsSaucers(trans)
return trans
}
private fun shipsVsSaucers(trans: Transaction) {
knownObjects.ships.forEach { ship ->
shipVsSaucer(ship, trans)
}
}
private fun shipVsSaucer(ship: Ship, trans: Transaction) {
knownObjects.saucers.forEach { saucer ->
saucer.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithSaucer(saucer, trans)
}
}
private fun asteroidsVsShipsAndSaucers(trans: Transaction) {
knownObjects.asteroids.forEach { asteroid ->
asteroidVsShip(asteroid, trans)
knownObjects.saucers.forEach { saucer ->
saucer.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithSaucer(saucer, trans)
}
}
}
private fun asteroidVsShip(asteroid: Asteroid, trans: Transaction) {
knownObjects.ships.forEach { ship ->
ship.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithShip(ship, trans)
}
}
private fun missilesVsEverything(trans: Transaction) {
knownObjects.missiles.forEach { missile ->
knownObjects.ships.forEach { ship ->
missile.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithMissile(missile, trans)
}
knownObjects.saucers.forEach { saucer ->
missile.subscriptions.interactWithSaucer(saucer, trans)
saucer.subscriptions.interactWithMissile(missile, trans)
}
knownObjects.asteroids.forEach { asteroid ->
missile.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithMissile(missile, trans)
}
}
}
It seems like a good idea to me, I’ll continue the hard work. IDEA wants to extend SpaceObjectCollection and all kinds of weird things.
Here’s what I’ve got now:
fun changesDueToInteractions(): Transaction {
val trans = Transaction()
missilesVsEverything(trans)
asteroidsVsShipsAndSaucers(trans)
shipsVsSaucers(trans)
return trans
}
private fun shipsVsSaucers(trans: Transaction) {
knownObjects.ships.forEach { ship ->
shipVsSaucer(ship, trans)
}
}
private fun shipVsSaucer(ship: Ship, trans: Transaction) {
knownObjects.saucers.forEach { saucer ->
saucer.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithSaucer(saucer, trans)
}
}
private fun asteroidsVsShipsAndSaucers(trans: Transaction) {
knownObjects.asteroids.forEach { asteroid ->
asteroidVsShip(asteroid, trans)
asteroidVsSaucer(asteroid, trans)
}
}
private fun asteroidVsSaucer(asteroid: Asteroid, trans: Transaction) {
knownObjects.saucers.forEach { saucer ->
saucer.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithSaucer(saucer, trans)
}
}
private fun asteroidVsShip(asteroid: Asteroid, trans: Transaction) {
knownObjects.ships.forEach { ship ->
ship.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithShip(ship, trans)
}
}
private fun missilesVsEverything(trans: Transaction) {
knownObjects.missiles.forEach { missile ->
missileVsShip(missile, trans)
missileVsSaucer(missile, trans)
missileVsAsteroids(missile, trans)
}
}
private fun missileVsAsteroids(missile: Missile, trans: Transaction) {
knownObjects.asteroids.forEach { asteroid ->
missile.subscriptions.interactWithAsteroid(asteroid, trans)
asteroid.subscriptions.interactWithMissile(missile, trans)
}
}
private fun missileVsSaucer(missile: Missile, trans: Transaction) {
knownObjects.saucers.forEach { saucer ->
missile.subscriptions.interactWithSaucer(saucer, trans)
saucer.subscriptions.interactWithMissile(missile, trans)
}
}
private fun missileVsShip(missile: Missile, trans: Transaction) {
knownObjects.ships.forEach { ship ->
missile.subscriptions.interactWithShip(ship, trans)
ship.subscriptions.interactWithMissile(missile, trans)
}
}
Let’s reflect.
Reflection
This all seems to work perfectly well. It is a ton more code than the original pairs thing. Seventy lines versus about fourteen, counting the pairs generation in knownObjects
.
I think each level of theseVsThose
makes sense, though I might prefer to reorder the methods for easier finding and reading.
But seventy lines versus fourteen, that seems heavy. This is the first time that the move toward centralization and knowing all the types has really cost us anything. It’s five times larger!
When it was all in line, it was … let’s see … 35 vs 14. Not quite so bad. Same exact functionality, with a few extra method calls threaded in. I think I prefer the highly-factored version, however. It’s more in the style to which I am accustomed rather than that long spate of nested loops.
I suspect what we have here is a need for a new object. We’ll leave that, and the creation of some new tests, for another day.
Oh, and here’s an idea that will pay off immediately! Game has ship and saucer. We don’t have to do that looping for those. Fix that:
Oh. No, we can’t do that. When there is no ship in the mix, the game still holds it. It’s just that it’s dead. Belay that order.
Everything is working, I believe, but I am seeing an odd effect. It looks as if when a missile times out, it generates a splat as usual … but then generates another splat, starting from where it timed out, a second or so later. I really don’t think this is my bug. However …
I’m going to treat this entire exercise as a spike and roll it back. I’ll do it again next time. Shouldn’t take long. I’ll try to do it better. You owe me a drink for having the courage to roll all this back!
The good news is, the splat duplication is still there. I didn’t cause it with my new interaction stuff. Makes sense to me.
See you next time!