Tozier is joining me today, and although we have a fun thing to do in the explosion, I’m inclined to work on damage for a bit. We have plenty of reason to believe that the collision logic isn’t quite right, and possibly dealing damage will be the right way to make some progress and see how to improve the design.

I’m inclined to think that the amount of damage something does is a property of the thing. That is, a missile might do one point of damage upon striking a ship, and a ship might do five points upon striking another ship. If we had a rule like that, the game would go on for a while, since it would take five hits with missiles to take out the enemy. I’m going to suggest that we try to make that happen.

We see these issues:

  • Missile does 1 point damage, ship can take 5
  • Ship might display points remaining for a while after being hit
  • Debris from explosions might do damage if they hit a ship.

These are all part of a “damage epic” but we’ll treat them as different stories.

We decide that in Ship:hitBy we’ll ask the object hitting us for the amount of damage dealt and apply it:

function Ship:hitBy(anObject)
    self.hitpoints = self.hitpoints - anObject.damageDealt
    if self.hitpoints <= 0 then 
        self:die()
    end
end

Both the ship and the missile need damageDealt. We set those to 5 and 1 respectively:

Ship = class()

    Ship.MaxMissiles = 50
    Ship.TicksBetweenMissiles = U.MissileKillDistance + 1
    Ship.ThrustAmount = vec2(0,0.02)
    Ship.TurnAmount = 1
    Ship.MaxSpeed = 3
    Ship.MaxDamage = 5

function Ship:init(shipNumber)
    self.name = string.format("Ship %d", shipNumber)
    self.missileLoad = Ship.MaxMissiles
    self.timeLastFired = -Ship.TicksBetweenMissiles
    self.shipNumber = shipNumber
    self.hitpoints = Ship.MaxDamage
    self.damageDealt = 5
    
    self.pos = self:initialPosition()
    self.heading = self:initialHeading()
    self.vel = vec2(0,0)
    self.controls = { 
        left   = self:controlButton(HEIGHT/5),
        right  = self:controlButton(2*HEIGHT/5),
        thrust = self:controlButton(3*HEIGHT/5),
        fire   = self:controlButton(4*HEIGHT/5)
    }
    U:addObject(self)
end

Similarly for the missile. We’re noticing that there is a lot of commonality between these objects, which is making us think there should be some common superclasses. Soon. This code works, so we commit and push.

We decide to “bulletproof” the damage code by assuming that anything that doesn’t define damageDealt does zero:

function Ship:hitBy(anObject)
    self.hitpoints = self.hitpoints - (anObject.damageDealt or 0)
    if self.hitpoints <= 0 then 
        self:die()
    end
end

We tested this. I note that if my tools were fast and convenient, I’d commit and push again. I do not, because it’s rather a big deal to do. This is troubling and gives one to think.

Missiles are currently dying for the wrong reason: because they got hit, not because they took one point. Tozier thinks this is a fine point and we might not worry about it. I agree, mostly, but we haven’t settled what happens if a Fragment gets hit by a Missile, and such. More to my interest is that if we add in this logic, we’ll increase duplication, which will increase the pressure to refactor to remove it.

function Missile:hitBy(anObject)
    self.hitpoints = self.hitpoints - (anObject.damageDealt or 0)
    if self.hitpoints <= 0 then 
        self:die()
    end
end

My view is that we should test this and then refactor out the duplication. Tozier seems to wonder how we’d test whether this changes anything: how can we tell whether it’s really there? His question made me wonder how we could automate this testing.

As a stopgap, if we give missiles 2 hitpoints, then we can’t shoot one down with one shot. We’ll try that manually.

Welcome back after a Very Long Period Of Confusion. We gave each missile two hit points, but when we fired one missile at another, they both died immediately. This sent us down a long spiral of thinking our interaction loop was somehow making everyone interact twice, because we’ve had that problem in the past. After Way Too Long, Tozier realizes that the missiles are still in range of each other, and get two chances to deal damage.

This is weird. Why are we so easily confused by all this? Is it evidence that we need more automated checks? Probably. But now we’re too tired to write one. Anyway we’re convinced that this works. We’ll back out the two hit points on the Missile, and all our ridiculous debugging prints, and commit.

This sort of thing really takes the steam out of our sails. I was all ready to remove some duplication. Now my gumption has got up and went.

The next day ..

We decide that we want to remove some of the duplication in our objects, notably the flying ones, probably by building a superclass, one class to rule them all. There are probably other clever ways to provide common behavior but I’m an OO kind of guy so the superclass makes sense to me.

We did a quick digression to build a function to return the names of the functions in a class. That was amusing but not as useful as just looking. (Tozier says “no more useful”, incorrectly in my opinion.) Anyway let’s just remove some duplication. In fact, let’s not. Despite my intuitive feeling that we need some kind of container class for these guys, the code just isn’t showing it. They do share a common “interface” but they do not share all that much common code, quite possibly none at all. OK, plan B, let’s build something.

Tozier suggests gravity, which I take to mean we need to build the Sun. The Sun has two aspects, at least. First, it is visible in the center of the screen. Second, it applies acceleration to ships (and perhaps to missiles and fragments). We discuss whether the gravity might be done without the Sun but we decide to build a simple display and go from there. I am concerned with this decision because the graphical sun has almost no connection to the thing that makes gravity. But hey, maybe the Sun isn’t in the middle. So we’ll build it and then see what to do with it.

After more stupidity than I’m willing to admit, we have:

Sun = class()

function Sun:init()
    self.pos = vec2(WIDTH/2, HEIGHT/2)
    U:addObject(self)
end

function Sun:draw()
    pushMatrix()
    pushStyle()
    strokeWidth(2)
    translate(self.pos:unpack())
    stroke(216, 168, 48, 255)
    fill(216, 168, 48, 255)
    ellipse(0,0,10)
    popStyle()
    popMatrix()
end

function Sun:move()
end

-- Main

function setup()
    parameter.action("Automate", function()
        automate()
    end)
    Sun()
    Ship1 = Ship(1)
    Ship(2)
end

IMAGE HERE

OK. Now gravity. Each ship needs to experience an acceleration due to gravity. Do I correctly recall that the acceleration is a constant independent of distance? What about that inverse-squared thing? I think it’s inverse squared. But Tozier points out that many of the practicalities don’t depend on the precise nature of the universe. This is profound but anyway let’s just give the ship a nudge in the direction of the sun. This will be put in Ship:move():

function Ship:move()
    local turn
    local thrust
    
    if     self.controls.left.pressed  and not self.controls.right.pressed then turn =  Ship.TurnAmount
    elseif self.controls.right.pressed and not self.controls.left.pressed  then turn = -Ship.TurnAmount
    else turn = 0 end
    self.heading = self.heading + turn
    
    if self.controls.thrust.pressed then thrust = Ship.ThrustAmount else thrust = vec2(0,0) end
    self.vel = self:adjustedVelocity(self.vel, self.heading, thrust)
    self.pos = U:clip_to_screen(self.pos + self.vel)
    
    if self.controls.fire.pressed then self:fireMissile() end
end

Looking at that, I want to put gravity into adjustedVelocity:

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    local speed = proposed:len()
    if speed > Ship.MaxSpeed then
        proposed = proposed*Ship.MaxSpeed/speed
    end
    return proposed
end

Yes, there we go. We want to add in acceleration due to gravity there in that proposed line. Or after it …

I promised to code more by intention:

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    proposed = proposed + self:gravity()
    local speed = proposed:len()
    if speed > Ship.MaxSpeed then
        proposed = proposed*Ship.MaxSpeed/speed
    end
    return proposed
end

See? self:gravity()! This is practically solved now.

function Ship:gravity()
    local directionToSun = (U.sun.pos - self.pos):normalize()
    local distSquared = self.pos:distSqr(U.sun.pos)
    return U.GravitationalConstant*directionToSun/distSquared
end

-- Universe

function Universe:init(x)
    self.Adds = {}
    self.Drawn = {}
    self.Removes = {}
    self.Touched = {}
    self.MissileKillDistance = 20
    self.GravitationalConstant = 100
    self.Tick = 0
end

And it looks fairly good:

MOVIE HERE

Time to push code. And done.

Tozier thinks gravity should affect missiles, or at least fragments. I’m OK with that, and it will lead to more duplication, which will increase pressure to remove duplication. OK, fragments first.

With a bit of copy and paste (another danger signal) we get to this point:

function Fragment:adjustedVelocity(vel)
    proposedV = vel + self:gravity()
    return proposedV
end

And we need a self:gravity(). Let’s at least centralize that … in the Universe! Tozier wants to make it work. Well, we have the implementation hat on so we shouldn’t refactor. I agree even though I’d rather leap into space assuming that a branch will appear out there somewhere. So first, Fragment gravity.

function Fragment:gravity()
    local directionToSun = (U.sun.pos - self.pos):normalize()
    local distSquared = self.pos:distSqr(U.sun.pos)
    return U.GravitationalConstant*directionToSun/distSquared
end

We left the maximum speed out of Fragment. That wasn’t a great idea:

MOVIE HERE

The gravity function is duplicated. Does this call for a common superclass? I think not in this case. Instead, I suggest that gravity is a property of the universe, not of a ship or a fragment or a common superclass. Thus:

function Universe:gravity(anObject)
    local directionToSun = (U.sun.pos - anObject.pos):normalize()
    local distSquared = anObject.pos:distSqr(U.sun.pos)
    return U.GravitationalConstant*directionToSun/distSquared
end

function Fragment:adjustedVelocity(vel)
    proposedV = vel + U:gravity(self)
    return proposedV
end

This should work … and it does. Now we change Ship:

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    proposed = proposed + U:gravity(self)
    local speed = proposed:len()
    if speed > Ship.MaxSpeed then
        proposed = proposed*Ship.MaxSpeed/speed
    end
    return proposed
end

This, too, should work … and it does. Tozier insists on doing maximum velocity, having talked me out of it earlier. “But here’s the thing”, he says … meaning, after some interrogation, where does it go? I answer that it is a property of the universe. We think this in perfect unison.

We start with more copy and paste:

function Fragment:adjustedVelocity(vel)
    proposedV = vel + U:gravity(self)
    local speed = proposedV:len()
    if speed > Ship.MaxSpeed then
        proposedV = proposedV*Ship.MaxSpeed/speed
    end
    return proposedV
end

This is unfortunate: the fragments, if subject to gravity, just stick to the sun when we crash into it:

MOVIE HERE

We fiddle a bit with the maximum speed of a fragment, to get this:

function Fragment:adjustedVelocity(vel)
    local max = 8
    proposedV = vel + U:gravity(self)
    local speed = proposedV:len()
    if speed >max then
        proposedV = proposedV*max/speed
    end
    return proposedV
end

MOVIE HERE

So … that’s not bad but it is weird. Ships flying that fast would surely be too fast to control. Fragments limited to ship speed don’t deal well with hitting the sun. And the basic form of the function is the same. We can move the function to universe and call back to the object for its maximum speed. At least that would centralize more of the code.

function Fragment:init(fragmentNumber, ship)
    self.type = fragmentNumber
    self.cannotCollide = true
    self.pos = ship.pos
    local speed = 1 + math.random()
    local outwardVelocity = vec2(0,speed):rotate(math.random()*math.pi*2)
    self.vel = ship.vel + outwardVelocity
    self.angularVelocity = math.random()*10-5
    self.heading = 0
    self.speedLimit = 8
    self.timeOut = math.random(200,300)
    U:addObject(self)
end

function Fragment:adjustedVelocity(vel)
    proposedV = vel + U:gravity(self)
    return U:limitSpeed(self, proposedV)
end

function Universe:limitSpeed(anObject, vel)
    local speed = vel:len()
    local max = anObject.speedLimit
    if speed > max then
        vel = vel*max/speed
    end
    return vel
end

This works fine. Let’s fix the ship to do the same … in doing so, we note that Ship has a “class variable” for its speed limit, so we change Fragment to work the same way, with this result:

function Ship:adjustedVelocity(vel, heading, accel)
    local proposed = vel + accel:rotate(math.rad(heading))
    proposed = proposed + U:gravity(self)
    return U:limitSpeed(self, proposed)
end

function Fragment:adjustedVelocity(vel)
    proposedV = vel + U:gravity(self)
    return U:limitSpeed(self, proposedV)
end

function Universe:limitSpeed(anObject, vel)
    local speed = vel:len()
    local max = anObject.MaxSpeed
    if speed > max then
        vel = vel*max/speed
    end
    return vel
end

And in Fragment we add the “class variable:

Fragment = class()

Fragment.MaxSpeed = 8

Fragment.types = {}
...

And with that, we declare ourselves done for the day and push the code.

Now for a brief Ruh-roh-spective.

We were confused at the beginning, because we forgot that the Universe has to create a sun before there is one. So we kept trying to make it more visible when it wasn’t being drawn at all. We can’t think of a clean way to ensure that this happens. Of course if we had written a test it would have required us to create one. That might have helped. Or if we had really started by intention, we’d have started in the Universe, creating a non-existent Sun instance, driving creation of the tab … let’s try to remember to start in the Universe in future. I am not planning to be any smarter tomorrow, so this may not work.

There still seem to us to be lots of similar pieces of code lying about. It bothers T that we pass to the speed limit code both the object and its proposed velocity. “Would it be better if we set the proposed into the velocity and then limited it by having the universe hammer it?” “Would it be better to pass the maximum instead of ripping it out?” Ah, yes. Law of Demeter. We probably should pass the maximum into this common universal function. Make a note of that.

We didn’t get in much trouble today … but we didn’t change our practice much from prior days. Were we just lucky?

Tozier questions whether a distance-squared gravity is ideal. I think he wants more gravity the further away you get or something. Maybe negative gravity? At one point he suggested gravity pulling straight down the screen, so the ships tend to pile up at the bottom. Of course, since the universe is a torus maybe a sun in the corner. No, stop. Just stop.

We don’t think we’ve learned a lot today: we just got some nice work done.

See you next time …