I’m sure my double dispatch approach will work fine. But the pattern seems simpler than that …

Musings

N.B. Seriously consider skipping down to “Never underestimate the value of a break”, unless you want to read a stream of consciousness which is accurate but wandering, as consciousness does. Mine, anyway.

I was delighted yesterday when Bill Wake (@wwake) commented on yesterday’s article. Bill’s an outstanding programmer, agilist, author, and mentor, and he even said nice things.

He also reminded me of a key fact: the pattern for “who does what to whom” in this game is very simple and quite symmetrical. If I were to write it all down at once, it might go like this:

  • Two objects of the same kind do not interact. (Unless we decide to allow shooting down missiles.)
  • Some objects don’t interact with anything. (Explosions, Splats)
  • Any two other objects, if close enough together, mutually destroy each other.

It’s generally considered to be bad practice to ask objects their type or class. It’s considered to be better design to build things so that you send messages to objects and they do the right thing. If we write code that is conditioned on the type of an object, that code almost certainly pertains specifically to that type, and therefore “should” be part of it.

We have this rather simple design, that generally requires us to know that two object are of different type if they are going to interact. The best pattern I know for that is double dispatch. We send a generic message to one of them, including the other, and the one sends a message to the other admitting its type or some other refining information. Then the other has enough information to decide what to do.

Yesterday’s work convinced me that we can readily do that, and that we’re going to find up with methods like this in each class, take “A” as an example

  • collide(obj), which sends obj:collideWithA(self),
  • collideWithA(obj), which does nothing,
  • collideWithB(obj), which destroys both ojbects,
  • collideWithC(obj), which destroys both objects,
  • collideWithD(obj), which destroys both objects.

The objects that can be destroyed will all look like that. Fairly simple, and will surely work. But is it really simple? No, certainly not. There will be five methods in each of the four mutual destructible classes. In each case there is one right way and three wrong ways to set up those five methods. Not what I’d call Really Simple.

But what would be better, darn it?

Bill suggested a thing “how do you behave on collision?” And he said “in some domains, you can do operation(f(x), g(x))”. He envisioned my table as an “operator table”, rather like a times table, a*b and so on.

There is no doubt that what we have here is like that. AM destroys, AA does not, M*A does, M*M does not. It’s like a …

It’s like exclusive or! Suppose each of the types had a number, and you xor’d them together. Then A xor A would be zero but A xor M would be non zero. This is just type checking by another name, of course. Is it “simpler”? It’s surely less code.

Handling the other objects, which we’re assuming are in the same collection, would be tricky, because they’d have to xor as zero against everyone. But we could do it in methods, perhaps.

How do you behave on collision … ah, that’s not the right question. “Tell, don’t ask”.

Right now the design is that an object gets the collide message and tells the other guy, in essence, “I’m a missile and I hit you”. What if, instead, he said, “I’m a mutual destroyer with asteroids, ships, and saucers”, and the other object said to itself, well, I fit that pattern, here we go.

Interesting. But no. It requires every object to know all the classes that exist. No, we already have to know that. Every time we add another object that interacts, our present scheme requires us to add knowledge of that object to all the other interacting classes. It’s only a question of how we do it, as methods or data.

Doing it with data, in a lookup table, as Bill seems to suggest, is very tempting. It would isolate the concerns.

What about a “Collider” object of some kind? It might go like this:

  • The main loops produce objects p and q to check for collision.
  • Create a Collider(p,q)
  • The collider says p:collide(self), q:collide(self)
  • p and q announce themselves back to the collider
  • the collider now knows what it needs to embody any collision policy we can imagine.

This is still just a roundabout way of determining the class or type of an object. But it might be better …

Never underestimate the value of a break …

N.B. Seriously consider skipping down to “Options”. This is still pretty speculative.

I’m back from a brief break, with a banana and a granola bar. And an idea.

There are only five cases for destruction:

  • nothing destroys me
  • ship: asteroids, missiles, saucers destroy me
  • asteroid: missiles, ships, saucers destroy me
  • saucer: missiles, ships, asteroids destroy me
  • missile: asteroids, ships, saucers destroy me

We could make five things … objects, tables, classes, each with one of those five behaviors built in. When a collision needs to be checked, we send collide to one of them and he returns his table, and then the other guy shoots at that table …

Hmm. Let’s consider Asteroid, hit by:

  • Asteroid: nothing.
  • Missile, Ship, Saucer: mutual destruction.

What about a table:, called a collider:

asteroidCollider = {
  asteroid=RETURN,
  missile=MAD,
  ship=MAD,
  saucer=MAD
}

function MAD(a,b)
  U:destroyBoth(a,b)
end

function RETURN(a,b)
end

The top level collision logic is (mostly):

for a,p do
   for b,q do
    local collider = b:collider()
    local projectile = a:projectile()
    action = collider[projectile]
    action(p,q)
  end
end

It’s still sort of answering class name, but not quite, since we could, for example, create instances of Missile that had projectile names “shipKiller” and “saucerKiller” depending on who had fired them.

This could work, I think. It’s also premature, since just now we have no need of it. We’re nearly there, however, and we could get to needing it quickly if we wanted to, by beginning to use our new objects collection instead of the specialized ones.

Let’s think about some options:

Options

Regarding collision:

  • Assume we can solve the collision problem, since we now have at least two ways, the Collider idea and the double dispatch idea. The latter has been shown to work by our tests from last time.
  • Push the Collider idea as far as we did with double dispatch. That would let us compare the two ideas more fairly.

Of these two, I prefer the latter. The idea is fresh in my mind and I’m curious to find out what it would really look like. But it does feel a bit premature.

Regarding the objects collection:

  • Push now to using the objects collection instead of the specialized ones. Make the code work either by selecting from objects by type, or by building in small versions of whatever collision approach we want to use.
  • Refactor the collision logic more slowly, extracting bits and improving them one at a time, removing duplication as we discover it, until it converges somewhere nice.

Of these, my normal operational mode would be the latter. I always prefer to refactor incrementally and see how good code emerges as we clean things up. Generally, good abstractions appear as needed.

However, we are in the middle of a committed notion, the single collection for all colliding objects (or, at present, all objects, colliding or not), and it would be nice to progress that effort further. Doing so would soon make the need for a solution to collision, at which point we could do what seemed best.

To decide, let’s look at where we stand on the objects collection.

Object Collections

I think we have completed centralizing the adding and deleting of asteroids, explosions, missiles, ships, and saucers. We’ve left splats out of the equation for now.

The result so far is a proliferation of methods in the Universe like this:

function Universe:addAsteroid(asteroid)
    self.objects[asteroid] = asteroid
    self.asteroids[asteroid] = asteroid
end

function Universe:deleteAsteroid(asteroid)
    self.objects[asteroid] = nil
    self.asteroids[asteroid] = nil
end

There’s a bit of special handling in deleteShip that would have to be dealt with, but that should be left to the ship.

Everything is now being stored both in the general collection objects and in its corresponding specific collection. We’ve even put the single ship and single saucer into the objects collection.

When we’re finished with this bit, there will only be two methods on Universe for this purpose, addObject and deleteObject, and everything of interest will be dealt with from there.

What would it take to convert to using the objects collection?

The collision logic looks like this:

function Universe:findCollisions()
    -- we clone the asteroids collection to allow live editing
    local needNewWave = true
    for i,a in pairs(clone(self.asteroids)) do
        needNewWave = false
        self:checkMissileCollisions(a)
        if self.ship then self:checkShipCollision(a) end
    end
    if needNewWave == true then
        if self.timeOfNextWave == 0 then
            self.timeOfNextWave = self.currentTime + self.timeBetweenWaves
        end
    end
end

function Universe:checkShipCollision(asteroid)
    if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
        asteroid:score()
        asteroid:split()
        self:deleteShip(self.ship)
    end
end

function Universe:checkMissileCollisions(asteroid)
    for k,m in pairs(self.missiles) do
        if self.ship and m.pos:dist(self.ship.pos) < self.ship:killDist() then
            self:deleteShip(self.ship)
            m:die()
        end
        if m.pos:dist(asteroid.pos) < asteroid:killDist() then
            asteroid:score()
            asteroid:split()
            m:die()
        end
    end
end

We could convert this to use the central collection immediately, by adding helper methods like Universe:asteroids that selected asteroids from the objects. We could do the same for missiles, and retain the special storage of the ship in a member variable. The collision code ignores the saucer at the moment: we cannot as yet shoot it down. (It would be easy enough to add, and if we need an excuse to clean up this code, adding saucer hits would be a good one.

I’m so tempted to just go in and rewrite the whole thing. In this case, I feel that I could surely do it in a couple of hours, and if not, I could revert and either do it again or go some other direction with more understanding tomorrow.

For this program, that would be OK. For a real legacy program, the rewrite might take longer. I know of one that took 42 elapsed days. I know of one that took a year. I know of some that never completed.

My advice is “never rewrite, always refactor”. I guess I have to stick with that.

Let’s refactor a while, while keeping all these ideas near our mind. Improving this code can only help with the decisions.

Refactoring Collisions.

Look at that first method:

function Universe:findCollisions()
    -- we clone the asteroids collection to allow live editing
    local needNewWave = true
    for i,a in pairs(clone(self.asteroids)) do
        needNewWave = false
        self:checkMissileCollisions(a)
        if self.ship then self:checkShipCollision(a) end
    end
    if needNewWave == true then
        if self.timeOfNextWave == 0 then
            self.timeOfNextWave = self.currentTime + self.timeBetweenWaves
        end
    end
end

Get serious, that’s doing two very different things, checking collisions, and deciding whether there should be a new wave and setting the new wave timer. That latter bit is just answering the question: are there any asteroids left and using the answer.

If we have our asteroids along with all the other objects in a single collection, finding out if there are any asteroids is in fact potentially costly. But this code is too complex. Let’s break it apart, leave it expensive for now, and deal with the cost separately:

At the end of our draw method we have this:

    self:drawMissiles()
    drawSplats()
    self:drawScore()
    popStyle()
    self:findCollisions()
end

We’ll add to that: self:checkNewWave, and implement that, for now, this way:

function Universe:checkNewWave()
    local count = self:asteroidCount()
    if count == 0 then
        if self.timeOfNextWave == 0 then
            self.timeOfNextWave = self.currentTime + self.timeBetweenWaves
        end
    end
end

function Universe:asteroidCount()
    local c = 0
    for i,a in pairs(self.asteroids) do
        c = c + 1
    end
    return c
end

We have asteroidCount lying around unused anyway. Now findCollisions:

function Universe:findCollisions()
    -- we clone the asteroids collection to allow live editing
    for i,a in pairs(clone(self.asteroids)) do
        self:checkMissileCollisions(a)
        if self.ship then self:checkShipCollision(a) end
    end
end

That’s better already. Let’s see if it works. I feel confident.

That works fine. I notice that the saucer kills a lot of asteroids. Almost makes me think his missiles shouldn’t do that, it’s almost too helpful. Anyway we can commit: “new wave separate from findCollisions”.

Now What?

You know, the more I think about this, the more troubled I am by one aspect of the single collection idea. In space, there are many asteroids, many missiles, one ship and at most one saucer.

Maybe this isn’t optimal …

It does make sense to check all the asteroids against all the missiles, and against the ship and the saucer. It does not make sense to check them against themselves. (It may, however, make sense to check the missiles against each other: it’d be kind of neat if you could shoot down that missile from the saucer.)

On the other hand, I think there are at most 44 asteroids, and if we followed the original game, at most 4 missiles from the ship. I ‘m not sure if the saucer could have more than one on screen or not. It’s not very many objects. Less than 350 comparisons, and usually more like 100, at a guess.

Ah. Think of it this way:

Keeping saucers and ships separate, keeping missiles and asteroids separate, is an optimization. We should do optimizations when a measurement tells us one is needed. Meanwhile, having done the optimizations, somewhat by accident, our code is messy.

We don’t worry about optimal when we’re worrying about clean design.

We’ll clean the code, confident that we can devise an optimization if we need one. As you were.

First we’ll switch the outer loop to use the objects collection.

What if we changed the outer loop right now to use the objects collection (but cloned, because we know we need to do something about adding and removing from a live collection (like not do it)).

Let’s do that and see what breaks.

function Universe:findCollisions()
    -- we clone the asteroids collection to allow live editing
    for i,a in pairs(clone(self.objects)) do -- <----
        self:checkMissileCollisions(a)
        if self.ship then self:checkShipCollision(a) end
    end
end

What breaks is:

Universe:205: attempt to call a nil value (method 'score')
stack traceback:
	Universe:205: in method 'checkShipCollision'
	Universe:199: in method 'findCollisions'
	Universe:80: in method 'draw'
	Main:12: in function 'draw'

That line has decided that an asteroid has hit the ship and intends to destroy it and the ship:

function Universe:checkShipCollision(asteroid)
    if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
        asteroid:score() -- line 205
        asteroid:split()
        self:deleteShip(self.ship)
    end
end

I reckon we are colliding the ship with itself. No one else was near it. How about adding this:

function Universe:checkShipCollision(asteroid)
    if not asteroid:is_a(Asteroid) then return end
    if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
        print(tostring(asteroid))
        asteroid:score()
        asteroid:split()
        self:deleteShip(self.ship)
    end
end

We just don’t check if it isn’t an asteroid. At some future time we’ll improve that, is our theory. Right now, we’re trying to get to using our central collection.

With this in place we get this:

Universe:219: attempt to call a nil value (method 'killDist')
stack traceback:
	Universe:219: in method 'checkMissileCollisions'
	Universe:198: in method 'findCollisions'
	Universe:80: in method 'draw'
	Main:12: in function 'draw'

We can likely fix this by checking for an asteroid in checkMissileCollisions, similarly to what we did above. It’s tempting to wonder what a large negative kill radius would do, but probably nothing good.

function Universe:checkMissileCollisions(asteroid)
    if not asteroid:is_a(Asteroid) then return end
    for k,m in pairs(self.missiles) do
        if self.ship and m.pos:dist(self.ship.pos) < self.ship:killDist() then
            self:deleteShip(self.ship)
            m:die()
        end
        if m.pos:dist(asteroid.pos) < asteroid:killDist() then
            asteroid:score()
            asteroid:split()
            m:die()
        end
    end
end

The first bit there is what lets saucer missiles kill the ship. It seems to have nothing to do with individual asteroids. This is happening on every asteroid, it appears. That’s hardly optimal. Anyway with the asteroid check here, everything works.

Let’s see about that redundant check of ship and missiles. We do want to do that ship check for every asteroid for ever missile, just for every missile. Let’s break that out:

function Universe:findCollisions()
    -- we clone the asteroids collection to allow live editing
    for i,a in pairs(clone(self.objects)) do
        self:checkMissileCollisions(a)
        if self.ship then self:checkShipCollision(a) end
    end
    if self.ship then
        for i,m in pairs(clone(self.missiles)) do
            self:checkMissileHitShip(m, self.ship)
        end
    end
end

function Universe:checkMissileHitShip(missile, ship)
    if  missile.pos:dist(ship.pos) < ship:killDist() then
        self:deleteShip(ship)
        missile:die()
    end
end

This works, though it took me a long time to be sure a missile still killed a ship. The saucer can’t hit a thing when you’re watching it.

Now we can change missiles loops similarly.

Now if we change the loops on self.missiles to self.objects, we’ll have to put in a couple more typechecks, but then we’ll be running on our single collection (and our distinguished ship and saucer variables).

A brief interlude of confusion.

OK this is weird. this code shows a confusing issue: it doesn’t seem to process

function Universe:checkMissileCollisions(asteroid)
    if not asteroid:is_a(Asteroid) then return end
    for k,m in pairs(self.missiles) do
        if m.is_a(Missile) then
            if m.pos:dist(asteroid.pos) < asteroid:killDist() then
                asteroid:score()
                asteroid:split()
                m:die()
            end
        end
    end
end

Excuse me. After way longer than I should admit, I noticed that says m.is_a, not m:is_a. I can’t even cheat right.

function Universe:checkMissileCollisions(asteroid)
    if not asteroid:is_a(Asteroid) then return end
    for k,m in pairs(self.objects) do
        if m:is_a(Missile) then
            if m.pos:dist(asteroid.pos) < asteroid:killDist() then
                asteroid:score()
                asteroid:split()
                m:die()
            end
        end
    end
end

This works, and I believe we are now running all the collisions on the objects collection, except for our use of the special variable ship.

Now we can remove all uses of the special collections.

Let’s look for other users of .missiles and .asteroids. We want to get rid of all of them before we move on. Ah. We change this:

function Universe:asteroidCount()
    local c = 0
    for i,a in pairs(self.objects) do
        if a:is_a(Asteroid) then c = c + 1 end
    end
    return c
end

We find the methods addAsteroid and deleteAsteroid and replace those and their callers with addObject and deleteObject.

function Universe:addObject(object)
    self.objects[object] = object
end

function Universe:deleteObject(object)
    self.objects[object] = nil
end

We realize we should have committed before we did that. Oh well …

We have this in drawAsterids:

function Universe:drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(self.asteroids) do
        asteroid:draw()
        asteroid:move()
    end
    popStyle()
end

That becomes:


function Universe:drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(self.objects) do
        if asteroid:is_a(Asteroid) then
            asteroid:draw()
            asteroid:move()
        end
    end
    popStyle()
end

We’re certainly getting a lot of type-checking in here. That’s not good but it’s what we expect for this phase.

We search for and find addAsteroid and fix it:

function Asteroid:init(pos, size)
    self.pos = pos or vec2(math.random(WIDTH), math.random(HEIGHT))
    self.scale = size or 16
    self.shape = Rocks[math.random(1,4)]
    local angle = math.random()*2*math.pi
    self.step = vec2(Vel,0):rotate(angle)
    U:addObject(self)
end

Same with deleteAsteroid:

function Asteroid:split()
    self:bang()
    Splat(self.pos)
    if self.scale ~= 4 then
        Asteroid(self.pos, self.scale//2)
        Asteroid(self.pos, self.scale//2)
    end
    U:deleteObject(self)
end

The only other reference to the asteroids collection is in the tests. It’s the newWave test and it is ignored. I think we’re done.

Tests run, game runs. Commit: “game uses object collection for asteroids and missiles, asteroids collection removed. missiles remains”.

Now the same for .missiles:

We have the same inits, removed. We removed the add and delete methods and make missile use the Object versions.

And we have this:

function Universe:drawMissiles()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    for k, missile in pairs(self.missiles) do
        missile:draw()
    end
    popMatrix()
    popStyle()
    for k, missile in pairs(self.missiles) do
        missile:move()
    end
end

Looking at Missile:draw, we find this:

function Missile:draw()
    ellipse(self.pos.x, self.pos.y, 6)
end

Let’s make him responsible for his own mess:

function Missile:draw()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    ellipse(self.pos.x, self.pos.y, 6)
    popMatrix()
    popStyle()
end

That leaves this:

function Universe:drawMissiles()
    for k, missile in pairs(self.missiles) do
        missile:draw()
    end
    for k, missile in pairs(self.missiles) do
        missile:move()
    end
end

We convert to objects at some expense:

function Universe:drawMissiles()
    for k, missile in pairs(self.objects) do
        if missile:is_a(Missile) then missile:draw() end
    end
    for k, missile in pairs(self.objects) do
        if missile:is_a(Missile) then missile:move() end
    end
end

The game runs. Two of our tests fail, the ones done yesterday that tested out the double-dispatch logic. They fail because the FakeUniverse doesn’t understand addObject.

I’m torn, because I think those tests are obsolete, but now’s not the time, so I change the fake addMissile to this:

function FakeUniverse:addObject(missile)
    self.missile = missile
end

And the tests and the game run. We’ve converted missile to use the objects collection. Commit: “missile uses objects collection”.

Time to break, reflect, and sum up:

Summing Up

Well. That was productive, though a bit long. I spent at least an hour at the beginning of the session, just considering options and rejecting most of them. I considered a few different paths, and finally chose the one based on principle.

I believe that all good designs can come about through small, simple refactoring. As tempted as I was to dive in and create some special collider object or go all the way to my double dispatch idea, I stuck with principle and started refactoring the collision logic.

It got improved by removing new wave detection, and we discovered one patch of code that was being done for every asteroid but only needed to be done once.

And we removed all references to the special collections asteroids and missiles. That allowed us to remove some type-specific methods like ‘addMissile”, at the present cost of doing run-time type checking in the collision loops, to be sure that our specialized code inside those loops never sees anything it shouldn’t.

And it all works. It’s better in that it is moving toward a design contraction due to removing specialized collections, and it’s worse due to the type checking.

Overall, I’m pleased. And despite the length of the session, The code never stayed broken very long and I almost never got confused.

Long one, though. I hope you skipped most of it, unless you just like to see me maundering.

Zipped Code