Kotlin 139
What if we run out of asteroids? What shall we do? Is a GameGod trying to be born? Sorry. Nope. Won’t do that. Minor deities, that’s one thing. One thing to rule them all? Nope.
I’m thinking that it might be time to create a second wave of asteroids after the first. I’ve looked at my Lua version and discovered some interesting things.
First, when asteroids are created in the Lua version, they always start at the screen edge, 50-50 between vertical edge and horizontal. Gives you a better chance. We might want to copy that idea.
Second, when a new wave is created, it will be double the size of the previous wave, up to a maximum of 10 asteroids.
I don’t know whether these notions are borrowed from the original game or not. I believe we’re currently starting with eight. My Lua version starts with four.
We want additional waves of asteroids, until the player runs out of ships. (Right now, you never run out, but that day will come.) When all the asteroids are gone, we want to rez N more, on the edges, and may the odds be ever in your favor. Haven’t we been here before? We do have an object that counts asteroids, the ShipMonitor. It presently counts only when it’s about to rez the ship back into the game.
- Pause
- I am fanatically dedicated to the idea of having no central god object controlling this game, simply because I want to see what happens with that style.
-
And yet, I find myself thinking how nice it would be if the main game loop would just count the asteroids. Does this mean that the god object design is better? In some ways, it surely is. In others, notably maintenance, it’s surely not. If you look at the original game source, with its branches and its rote attention to first these guys then these other guys, you can see how some changes could be quite difficult. Of course their processor ran about 60 cycles per second and they had only 13 bytes of memory or something like that, so compact code was the order of the day.
-
We are not there and we’re not going to pretend that we are. It’s bad enough that we’re implementing a game from 1979. But we are going to keep away from the central god approach.
How about another asteroid counter object? This one will only count the asteroids every X cycles, maybe once a second or once every three seconds. When it detects zero asteroids, it creates N more for some N.
While we’re at it, let’s change the game so that the initial asteroids start on the edges. It would be irritating to be catching your breath at the end of a wave and the game rezzes an asteroid right on top of you.
Let’s start with the edge thing. We create our initial asteroids with this code:
fun createContents(controls: Controls) {
val ship = newShip(controls)
add(ship)
add(ShipMonitor(ship))
add(ScoreKeeper())
add(LifetimeClock())
for (i in 0..4) {
val pos = U.randomPoint()
val vel = Velocity(1000.0, 0.0).rotate(Random.nextDouble(0.0,360.0))
val asteroid = SolidObject.asteroid(pos,vel )
add(asteroid)
}
}
Clearly that last bit wants to be pulled out into a method, to take a parameter saying how many, and to be modified to put them on the edge. This code is not tested and, please forgive me, I’m not going to write a test for it. I admit that I am a bad person. It’s early in the morning, and I want to have some fun. Fire me, but please wait until after lunch, I have a lunch date with that beautiful head of documentation who will one day in the distant past become my wife.
fun createContents(controls: Controls) {
val ship = newShip(controls)
add(ship)
add(ShipMonitor(ship))
add(ScoreKeeper())
add(LifetimeClock())
createEdgeAsteroids(4)
}
private fun createEdgeAsteroids(n: Int) {
for (i in 0..n) {
val pos = U.randomPoint()
val vel = Velocity(1000.0, 0.0).rotate(Random.nextDouble(0.0, 360.0))
val asteroid = SolidObject.asteroid(pos, vel)
add(asteroid)
}
}
Kotlin did most of that for me. Surely it works just fine. Now about the edge. We’re asking the universe for a random point (I think there are people doing that for themselves, we should look.) We’ll ask for a randomEdgePoint instead. I’m sure that the Universe will provide:
val pos = U.randomEdgePoint()
IDEA offers to create that for us. We fill in the details:
fun randomEdgePoint(): Point =
if (Random.nextBoolean()) Point(0.0, Random.nextDouble(UNIVERSE_SIZE))
else Point(Random.nextDouble(UNIVERSE_SIZE), 0.0)
The game now starts with four asteroids, coming in from the edges. Commit to that effect.
Now let’s make waves. We’ll TDD a new object, WaveMaker, that counts the asteroids and creates more if there are none. For now, we’ll have it create 8.
fun createContents(controls: Controls) {
val ship = newShip(controls)
add(ship)
add(ShipMonitor(ship))
add(ScoreKeeper())
add(LifetimeClock())
add(WaveMaker())
createEdgeAsteroids(4)
}
I just typed that in while I was thinking about it. I promise, we’ll test-drive this baby.
Two tests come to mind, so I create both their shells:
class WaveMakerTest {
@Test
fun `creates wave on update when no asteroids`() {
}
@Test
fun `counts asteroids`() {
}
}
The first one is easier, I think. Maybe like this:
@Test
fun `creates wave on update when no asteroids`() {
val wm = WaveMaker()
wm.asteroidCount = 0
val toCreate = wm.update(0.01)
assertThat(toCreate.size).isEqualTo(8)
}
I should mention that even though I often resist writing a test, I almost always find them useful. This one reminds me of roughly how WaveMaker will work, creating on update
as one does. It also reminded me, because I had to put in the time interval, that we really want it to have a delay. I won’t forget that, but I also won’t put it in the test yet, as I have creation on my mind.
Let’s let IDEA help us make this thing. It will be an ISpaceObject implementor, of course.
IDEA does this work for me:
class WaveMaker: ISpaceObject {
var asteroidCount = 0
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
TODO("Not yet implemented")
}
override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> {
TODO("Not yet implemented")
}
override fun update(deltaTime: Double): List<ISpaceObject> {
TODO("Not yet implemented")
}
}
I get to do the details. All I need here is to fill in a tiny bit of code in update
. I could put in more but let’s do the minimum to pass.
override fun update(deltaTime: Double): List<ISpaceObject> {
val list = mutableListOf<ISpaceObject>()
for (i in 1..8) {
val vel = Velocity(1000.0, 0.0).rotate(Random.nextDouble(0.0, 360.0))
val a = SolidObject.asteroid((U.randomEdgePoint()), vel)
list.add(a)
}
return list
}
I see we might be happy to have a randomVelocity function in U as well. I think this passes. It does. Let’s see what we really want …
I think that when the WaveMaker notices that there are no asteroids, it has a timer, timeSinceLastAsteroidPassedAllUntimely or something like that, and after three seconds of no asteroids poof, we do asteroids.
- Thinking
- I’m thinking. With the spec as written, the WaveMaker will have some tricky timing logic. It will want to check the count rarely, maybe once per second. Then, having found none, it needs to enter another mode, where it doesn’t bother to count but waits for three seconds to elapse. Then it goes back to counting.
-
What if we did it this way: We could have a WaveChecker and a WaveMaker. What I’d like is that the Checker sees that there are no asteroids, derezzes itself and in finalization, rezzes a WaveMaker. The WaveMaker would make a wave during its update and remove itself during its iteration loop.
-
Something like that. Let’s try it, it pushes the notion of these small smart objects toward the limit. I’ll revise the tests we have for the maker and do new tests for the checker.
@Test
@Test
fun `creates wave on update, removes self on interaction`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val wm = WaveMaker()
val toCreate = wm.update(0.01)
assertThat(toCreate.size).isEqualTo(8)
var toDestroy = wm.interactWithOther(wm)
assertThat(toDestroy[0]).isEqualTo(wm)
toDestroy = wm.interactWith(wm)
assertThat(toDestroy[0]).isEqualTo(wm)
}
I think this is the story. We won’t check to see that there are actually asteroids in the toCreate, we’ll trust ourselves. Should I not do that? Let’s talk about that later.
The code demands this in U:
fun randomWelocity(speed: Double): Velocity = Velocity(speed,0.0).rotate(Random.nextDouble(360.0))
}
There are others who can use that. We’ll look for them shortly. The test fails wanting the interactWithOther
. We’ll oblige.
class WaveMaker: ISpaceObject {
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
return listOf(this)
}
override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> {
return this.interactWith(other)
}
override fun update(deltaTime: Double): List<ISpaceObject> {
val list = mutableListOf<ISpaceObject>()
for (i in 1..8) {
val vel = Velocity(1000.0, 0.0).rotate(Random.nextDouble(0.0, 360.0))
val a = SolidObject.asteroid((U.randomEdgePoint()), vel)
list.add(a)
}
return list
}
}
Remember that update happens before interact. So this object, upon its first emergence, creates 8 asteroids and exits. Let’s modify it to take the number as a parameter. I have an idea.
Modify the test? Yes, let’s:
@Test
fun `creates wave on update, removes self on interaction`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val wm = WaveMaker(7)
val toCreate = wm.update(0.01)
assertThat(toCreate.size).isEqualTo(7)
var toDestroy = wm.interactWithOther(wm)
assertThat(toDestroy[0]).isEqualTo(wm)
toDestroy = wm.interactWith(wm)
assertThat(toDestroy[0]).isEqualTo(wm)
}
class WaveMaker(val numberToCreate: Int = 8): ISpaceObject {
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
return listOf(this)
}
override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> {
return this.interactWith(other)
}
override fun update(deltaTime: Double): List<ISpaceObject> {
val list = mutableListOf<ISpaceObject>()
for (i in 1..numberToCreate) {
val vel = Velocity(1000.0, 0.0).rotate(Random.nextDouble(0.0, 360.0))
val a = SolidObject.asteroid((U.randomEdgePoint()), vel)
list.add(a)
}
return list
}
This is green. Let’s use it in game creation:
fun createContents(controls: Controls) {
val ship = newShip(controls)
add(ship)
add(ShipMonitor(ship))
add(ScoreKeeper())
add(LifetimeClock())
add(WaveMaker(4))
}
Unless I miss my guess, the game will start up with four asteroids. It does. I love it when a plan comes together. We now have a WaveMaker, initialized with a number. When it is in the mix, it creates that number of asteroids and then bows out.
Now we need WaveChecker. How can it work? Objects can destroy themselves in interactsWith, and no other times. They can add objects in update, and finalize, and no other times.
I should mention that I’m beginning to think that on all three of those occasions we should return a list of transactions, add this, delete that sort of thing. But for now, this is what we’ve got.
We’ll enter first at update. So we must init the object to show at least one asteroid. Then in interaction, we’ll count them. Then in update, if we see no asteroids, we’ll do two things: we’ll rez a WaveMaker, and we’ll set a dieNow
flag. In interaction, if that flag is set, we return ourselves, the WaveChecker, to be destroyed.
Oh, and I forgot, we’ll want it to check only every second. I think we want check the time in update and use a flag to decide when to start looking … and that flag can be the asteroid count.
I code a bit more than I should:
class WaveChecker: ISpaceObject {
var asteroidCount = 1
override var elapsedTime = 0.0
private var missionComplete = false
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
if ( missionComplete ) return listOf(this)
if (other is SolidObject && other.isAsteroid)
asteroidCount += 1
return emptyList()
}
override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> {
return this.interactWith(other)
}
override fun update(deltaTime: Double): List<ISpaceObject> {
if (asteroidCount == 0) {
missionComplete = true
return listOf(WaveMaker())
}
elapsedTime += deltaTime
if (elapsedTime > 1.0) asteroidCount = 0
return emptyList()
}
}
My test is passing. No surprise, it’s pretty simple. Let’s go deeper.
@Test
fun `checker does nothing for one second, then counts asteroids`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val ck = WaveChecker()
assertThat(ck.asteroidCount).isNotEqualTo(0)
var toCreate = ck.update(0.51)
assertThat(ck.asteroidCount).isNotEqualTo(0)
assertThat(toCreate).isEmpty()
toCreate = ck.update(0.51)
assertThat(toCreate).isEmpty()
assertThat(ck.asteroidCount).isEqualTo(0)
assertThat(ck.elapsedTime).isEqualTo(0.0)
var toDestroy = ck.interactWith(a)
assertThat(toDestroy).isEmpty()
assertThat(ck.asteroidCount).isEqualTo(1)
}
To make that run requires this:
override fun update(deltaTime: Double): List<ISpaceObject> {
if (asteroidCount == 0) {
missionComplete = true
return listOf(WaveMaker())
}
elapsedTime += deltaTime
if (elapsedTime > 1.0) {
asteroidCount = 0
elapsedTime = 0.0
}
return emptyList()
}
Now one more test, passing a second and seeing no asteroids:
@Test
fun `checker creates WaveMaker and bows out`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val ck = WaveChecker()
var toCreate = ck.update(1.1)
assertThat(toCreate).isEmpty()
assertThat(ck.asteroidCount).isEqualTo(0)
assertThat(ck.elapsedTime).isEqualTo(0.0)
// see no asteroids
toCreate = ck.update(0.1)
assertThat(toCreate[0]).isInstanceOf(WaveMaker::class.java)
assertThat(ck.missionComplete).isEqualTo(true)
val toDelete = ck.interactWith(a)
assertThat(toDelete[0]).isEqualTo(ck)
}
I expect this to pass. It does. Commit: WaveChecker and WaveMaker passing all tests.
Now we need one of these babies in the game:
Oh! We shouldn’t destroy the WaveChecker. We have more than one wave. I had been thinking to destroy it and recreate but why not just reset it?
Change the tests.
@Test
fun `checker creates WaveMaker and resets`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val ck = WaveChecker()
var toCreate = ck.update(1.1)
assertThat(toCreate).isEmpty()
assertThat(ck.asteroidCount).isEqualTo(0)
assertThat(ck.elapsedTime).isEqualTo(0.0)
// see no asteroids
toCreate = ck.update(0.1)
assertThat(toCreate[0]).isInstanceOf(WaveMaker::class.java)
assertThat(ck.asteroidCount).isEqualTo(1)
assertThat(ck.elapsedTime).isEqualTo(0.0)
}
OK, I’m not proud of this. My tests ran. I ran the game and the game didn’t create a new wave. I debugged it with some prints. Discovered some flaws. Now the game runs and the tests don’t.
Let’s reflect on what I’ve learned and decide what to do about it:
Reflection
The first thing I learned was the the WaveChecker was creating WaveMakers on every cycle once it decided to do it at all. That led me to change its timing logic.
Then I finally figured out that while the official life cycle is update then interact, if an object is created by an update, its first encounter with the world will be in interact, because the loop will move next to interact.
So: objects created by update will interact before update. Objects created at game init time, or in finalize, will update then interact.
While I was watching that happen, I noticed also that the WaveMaker destroys itself once for every other object in the universe., since it destroys itself in the interaction loop. No surprise, but when you see it happening it’s kind of embarrassing.
Some thoughts stemming from this include:
- We shouldn’t have to think about how objects are created when we build them: they should all work alike. Presumably that should be update-then-interact.
- Writing tests for these objects is tricky, because they all go through a few internal states as they progress from update to interacting and decide what to do.
- It would be nice if it was easier to write an object that only processed every second or N seconds. If this were done well, it might help with the order of things. Suppose no object interacted until it had updated at least once, for example.
- We could save adds from update and only add them in before the next update. That would cause everyone to update before interacting.
- Objects have a finalize. Maybe they need an explicit init? But why can’t their creation handle that. Doing the preceding item might help.
- Something is pushing me to test in the game and to code before testing. Is it the long story-style tests? If not that … what?
Let’s divert and look at how hard it would be to add update results to the mix only right before the update.
fun update(deltaTime: Double) {
val adds = mutableListOf<ISpaceObject>()
knownObjects.forEach { adds.addAll(it.update(deltaTime)) }
knownObjects.addAll(adds)
}
Suppose we have a member addsFromUpdates, and use it this way:
class Game {
...
val addsFromUpdates = mutableListOf<ISpaceObject>()
...
fun update(deltaTime: Double) {
knownObjects.addAll(addsFromUpdates)
addsFromUpdates.clear()
knownObjects.forEach { addsFromUpdates.addAll(it.update(deltaTime)) }
}
Now we know that all objects will see update before they see interaction.
Test. I am sure we have no test for this. Perhaps we should.
- Even Longer Delay
- I’ve been futzing with this for way too long. But it’s working … in the game. The tests are way behind. Let’s view the code, see if we can recover to some decent testing. We’ll start with WaveMaker: it’s easier.
class WaveMaker(val numberToCreate: Int = 8): ISpaceObject {
override var elapsedTime = 0.0
var done = false
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
return if ( done ) listOf(this)
else emptyList()
}
override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> {
return this.interactWith(other)
}
override fun update(deltaTime: Double): List<ISpaceObject> {
elapsedTime += deltaTime
if (elapsedTime < 3) return emptyList()
val list = mutableListOf<ISpaceObject>()
for (i in 1..numberToCreate) {
val vel = Velocity(1000.0, 0.0).rotate(Random.nextDouble(0.0, 360.0))
val a = SolidObject.asteroid((U.randomEdgePoint()), vel)
list.add(a)
}
done = true
return list
}
}
We begin in update, because that change is in. In fact, hold on a second and I’ll commit it: Game ensures update before interaction.
So. We wait three seconds in update, done flag false all this time. When time’s up, we create our quota of asteroids and set done to true. Then, in interactWith, we return ourselves and bow out.
OK that makes sense. The only test runs:
@Test
fun `creates wave on update, removes self on interaction`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val wm = WaveMaker(7)
val toCreate = wm.update(3.01)
assertThat(toCreate.size).isEqualTo(7)
var toDestroy = wm.interactWithOther(wm)
assertThat(toDestroy[0]).isEqualTo(wm)
toDestroy = wm.interactWith(wm)
assertThat(toDestroy[0]).isEqualTo(wm)
}
Test is green. I’m going to commit that object. Commit: WaveMaker works.
Now the checker. It’s a lot more intricate, and I don’t like that:
class WaveChecker: ISpaceObject {
var firstTime = true
var lookingForAsteroid = false
override var elapsedTime = 0.0
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
if ( lookingForAsteroid && other is SolidObject && other.isAsteroid)
lookingForAsteroid = false
return emptyList()
}
override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> {
return this.interactWith(other)
}
override fun update(deltaTime: Double): List<ISpaceObject> {
val result = mutableListOf<ISpaceObject>()
elapsedTime += deltaTime
if (elapsedTime > 1.0) {
if (firstTime) {
lookingForAsteroid = true
firstTime = false
}else if (lookingForAsteroid) {
elapsedTime = -5.0
firstTime = true
lookingForAsteroid = false
result.add(WaveMaker(1))
} else {
elapsedTime = 0.0;
firstTime=true }
}
return result
}
}
I’ve worked with this in the game and I think it works. Its tests are a bit of a mess. Two fail:
@Test
fun `checker does nothing for one second, then counts asteroids`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val ck = WaveChecker()
assertThat(ck.lookingForAsteroid).isEqualTo(false)
var toCreate = ck.update(0.51)
assertThat(ck.lookingForAsteroid).isNotEqualTo(false)
assertThat(toCreate).isEmpty()
toCreate = ck.update(0.51)
assertThat(toCreate).isEmpty()
assertThat(ck.lookingForAsteroid).isEqualTo(true)
assertThat(ck.elapsedTime).isEqualTo(0.0)
var toDestroy = ck.interactWith(a)
assertThat(toDestroy).isEmpty()
assertThat(ck.lookingForAsteroid).isEqualTo(1)
}
Expecting actual:
false
not to be equal to:
false
That’s not very helpful. Best improve that.
@Test
fun `checker creates WaveMaker and resets`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val ck = WaveChecker()
var toCreate = ck.update(1.1)
assertThat(toCreate).isEmpty()
assertThat(ck.lookingForAsteroid).isEqualTo(0)
assertThat(ck.elapsedTime).isEqualTo(0.0)
// see no asteroids
toCreate = ck.update(0.1)
assertThat(toCreate[0]).isInstanceOf(WaveMaker::class.java)
assertThat(ck.elapsedTime).isEqualTo(0.0)
}
That runs green. I update the other test as well, to reflect what happens (and what works).
@Test
fun `checker creates WaveMaker and resets`() {
val a = SolidObject.asteroid(U.randomPoint(), U.randomWelocity(1000.0))
val ck = WaveChecker()
var toCreate = ck.update(1.1)
assertThat(toCreate).isEmpty()
assertThat(ck.lookingForAsteroid).describedAs("start looking").isEqualTo(true)
assertThat(ck.elapsedTime).describedAs("still ticking").isEqualTo(1.1)
// see no asteroids
toCreate = ck.update(0.1)
assertThat(toCreate[0]).isInstanceOf(WaveMaker::class.java)
assertThat(ck.elapsedTime).describedAs("long delay after triggering").isEqualTo(-5.0)
}
I am green. Let me reset some numbers and try the game.
Game is good … except that I have a ??? displaying when I go to hyperspace. I’m sure that’s the default object view … but it means that there’s someone in the mix who needs a view … probably the destructor … but why is it even visible?
First commit what we’ve got: WaveChecker and WaveMaker working and deployed in v 0.5
Now let’s see about the destructor. Where’s hyperspace?
private fun makeEmergenceObjects(): List<ISpaceObject> {
return when (emergenceIsOK()) {
true -> {
listOf(shipReset())
}
false -> {
val splat = SolidObject.splat(ship)
val destroyer = SolidObject.shipDestroyer(ship)
listOf(splat, destroyer, shipReset())
}
}
}
And shipDestroyer
…
fun shipDestroyer(ship: SolidObject): SolidObject {
return SolidObject(
position = ship.position,
velocity = Velocity.ZERO,
killRadius = 100.0,
)
}
Ah. The bug is that since we are adding two destroyers, because we don’t clear the hyperspace button, and we get around the loop once before the ship is gone, thus creating another destroyer. This is due to the change in the order of updating the adds to ensure update before interaction. Fix is this:
fun control(ship: SolidObject, deltaTime: Double): List<ISpaceObject> {
if (hyperspace) {
hyperspace = false
return listOf(SolidObject.shipDestroyer(ship,99.0))
}
turn(ship, deltaTime)
accelerate(ship, deltaTime)
return fire(ship)
}
Careful readers may recall that I speculated about the need to do that. Skeptical readers might ask me why I didn’t remember to test for it or do it. Good points, readers.
Point remains, we are green and our game is creating wave after wave of asteroids. We commit: Bug fixed that created extra destroyer.
Well past time to sum up.
Summary
I feel certain that this was harder than it would have been with a god object. We’d have surely had a place in the god object where the loop ended, and at that point we could have counted the asteroids and if there were none, set a timer, ticked it down, and started a new wave. Here, our rather cute implementation has a wave maker that just waits a discreet interval, then creates a batch of asteroids and dies. But even to do that is rather tricky:
class WaveMaker(val numberToCreate: Int = 8): ISpaceObject {
override var elapsedTime = 0.0
var done = false
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
return if ( done ) listOf(this)
else emptyList()
}
override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> {
return this.interactWith(other)
}
override fun update(deltaTime: Double): List<ISpaceObject> {
elapsedTime += deltaTime
if (elapsedTime < 3) return emptyList()
val list = mutableListOf<ISpaceObject>()
for (i in 1..numberToCreate) {
val vel = Velocity(1000.0, 0.0).rotate(Random.nextDouble(0.0, 360.0))
val a = SolidObject.asteroid((U.randomEdgePoint()), vel)
list.add(a)
}
done = true
return list
}
}
That object would be much simpler if we could return deletes as well as adds from update
: we’d just wait for the timer, create a list of asteroids, add a delete for this
to it, and return. Poof, done and gone. An argument for a transaction solution or equivalent. Maybe we need a kind of paired collection containing a set of adds and a set of deletes.
The WaveChecker is even more baroque:
class WaveChecker: ISpaceObject {
var firstTime = true
var lookingForAsteroid = false
override var elapsedTime = 0.0
override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
if ( lookingForAsteroid && other is SolidObject && other.isAsteroid)
lookingForAsteroid = false
return emptyList()
}
override fun interactWithOther(other: ISpaceObject): List<ISpaceObject> {
return this.interactWith(other)
}
override fun update(deltaTime: Double): List<ISpaceObject> {
val result = mutableListOf<ISpaceObject>()
elapsedTime += deltaTime
if (elapsedTime > 1.0) {
if (firstTime) {
lookingForAsteroid = true
firstTime = false
}else if (lookingForAsteroid) {
elapsedTime = -5.0
firstTime = true
lookingForAsteroid = false
result.add(WaveMaker(4))
} else {
elapsedTime = 0.0
firstTime=true }
}
return result
}
}
What are the issues here? I think the primary one is that an object like this one only gets the objects to collide with one at a time … and it really wants to know the result of the entire collision package: did we see any asteroids or not. Because of that we have to ping pong from update, where we want to wait a second between each check, and where we have to look after each check to see if any were found.
One thing that might help us here is for the Game to send every object collisionsOver
, which would of course be allowed to return deletes and adds. Then we could let the update just keep the time, set the “do it” flag and the collision logic would count asteroids and the collisionsOver
would, if there were none, create a maker.
But there is a glitch: the maker is going to wait three seconds before making … so the checker really needs not to check for longer than that, lest he create more makers. I’ve seen that happen and it’s not a pretty sight as the screen fills up with asteroids.
We could also deal with that by having the Checker delete itself and the Maker make a new one. Kind of like a flip-flop. Not the sandals, the logic gate.
I think we have a few more of these coming up. We need a finite number of waves, an attract mode, transition back to game mode. This inclines me to try to improve the situation to give these guys simpler states. There is still the issue that what they do is a bit mysterious. One object creating another, maybe that one creating the other back. It’s a bit odd.
I wonder whether a sort of composite set of objects might be easier. If the checker and maker were co-resident and knew each other, the checker might be able to just maintain a single fact “there were asteroids last time I looked”, and the maker could just check every three seconds and if there were none, spawn new ones.
That might be interesting. Maybe we’ll look at that next time or next but one, since next time is Sunday and I usually take on a lighter load on Sundays.
Bottom line, the “distributed” architecture is, I think, more difficult to work with than the monolithic, although the monobloc would have its own issues. The grass is always greener, and maybe the monobloc.
One step in the right direction, I think, is the change made today, that accumulates the addsFromUpdate
but doesn’t add them into the mix until the next update, which ensures that the first operation an added object sees is always update. We might wish to return both adds and deletes from all the phases, update, interact, and finalize … and then install them all at once. That way all changes are buffered until the next time through the game loop.
We’ll want to experiment with this. I think that god objects are generally not the best solution, but they are easy and tempting until they get hard to manage. So it would be good to have this more distributed scheme as smooth as possible.
Anyway, the evil of the day and so on. See you next time!