Not really an answer, but just a bit of fun.

I have a bit of fun in mind, but first a few words about some tweets from @Daganev. Daganev said:

I think the inheritance makes sense here. The one thing I would do differently is to only allow player missles to score points, ever.

The @sandimetz rule is inheritance should be wide and shallow. So, if you were to make it so that only “Missle” scores points the current inheritance doesn’t work. But I think you should make a PlayerMissle, that only scores points, and then the inheritance would be right.

And I replied:

only player missiles score points. It occurs to me that if I reversed the inheritance, Missile from Saucer, the “abstract” score defaults might serve for Saucer. It’d still need the check to kill only ships tho …

I meant to say there “SaucerMissile” of course.

Since we’re using inheritance consistently with the notion of “a programmer’s hack to save code”, we should take a look at whether this is the right order at all.

The “abstract” score in Destructible throws an error, because I wanted to be sure that everyone implemented score:

function Destructible:score()
    dump("score", self)
    assert(false)
end

Smalltalk used a similar trap in superclasses, called shouldImplement: I believe. So we’d have to override that in both methods anyway.

And SaucerMissile only implements two methods anyway:

SaucerMissile = class(Missile)

function SaucerMissile:collide(anObject)
    if anObject:is_a(Ship) then
        anObject:mutuallyDestroy(self)
    end
end

function SaucerMissile:mutuallyDestroy(anObject)
    if anObject:is_a(Ship) then
        if self:inRange(anObject) then
            self:die()
            anObject:die()
        end
    end
end

We have to test for Ship, as far as I can tell. So … I think this is as good as it gets. We can be reasonably confident that this isn’t going to bite us, because we’re nearing the end of all the interesting Asteroids functionality.

I appreciate the feedback from Dagonev, it’s always helpful to get a different perspective from which to look at things.

Now for some fun.

Ship Explodes

Our ship currently explodes by printing BLAMMO in outer space. This is not what we hope for from an explosion, even when it’s us. The original game had a brief animation of a few ship parts drifting apart. I believe I could eve find enough code and data to replicate those parts, but I have an idea that is much more fun.

But before we get to that (sorry about the clickbait), we might want to consider how Splat and Explosion work.

Splats own their own instances and manage them within the class. The main drawing loop calls drawSplats, a global in the Splat tab. (It could be changed, easily, to Splat:draw() if we felt that was better than the additional global.)

Explosion, on the other hand, is the only “Indestructible` object implemented. It does participate in collision logic, which it implements like this:

-- Indestructible

function Explosion:collide(anything)
    -- nope
end

function Explosion:mutuallyDestroy(anything)
    --nope
end

My original plan with Destructible was that there would be a hierarchy of “indestructible” objects as well. In retrospect, that’s a bit silly, because if they aren’t going to be able to be destroyed, there’s no reason to have them in the main objects collection at all. There could be a separate collection of objects, processed separately in Universe:draw, probably in here:

function Universe:drawEverything()
    for k,o in pairs(self.objects) do
        o:draw()
    end
end

function Universe:moveEverything()
    for k,o in pairs(self.objects) do
        o:move()
    end
end

We would simply loop over the indestructibles here, giving them their time on screen and the opportunity to move, but then when we do findCollisions they would be skipped, because they’re not in objects at all.

function Universe:findCollisions()
    for k, o in pairs(self.objects) do
        for kk, oo in pairs(self.objects) do
            o:collide(oo)
        end
    end
end

The savings in time wouldn’t be much, but the design might arguably be better. Let’s think about that.

We presently have two schemes for drawing things, Universe.objects and Splat. Splat could be duplicated for other objects but why have two entirely different ways, separated so far. And there’s a special call in Universe:draw to draw the splats anyway.

I think we’ll do it. I don’t see a great value to trying to write microtests for this. I’m just going to go ahead. Watch and see if I get in trouble.

We need a new collection in Universe, much like objects. It’ll be initialized and generally handled in the same places. I’ll do that first:

function Universe:drawEverything()
    for k,o in pairs(self.objects) do
        o:draw()
    end
    for k,o in pairs(self.indestructibles) do
        o:draw()
    end
end

function Universe:moveEverything()
    for k,o in pairs(self.objects) do
        o:move()
    end
    for k,o in pairs(self.indestructibles) do
        o:move()
    end
end

And there are two initializers, in init and startGame. I’ll spare you those,

Now we want to do the add and delete as we do for objects:

function Universe:addIndestructible(object)
    self.indestructibles[object] = object
end

function Universe:deleteIndestructible(object)
    self.indestructibles[object] = nil
end

We don’t have to buffer the indestructibles because we don’t add things while we’re looping over them. (Note that this could be a dangerous decision, but I’m here and I’m sure even if I’m not correct.)

Now at this point we should be able to edit Splat to use these. Let’s do it and then see if it works. Here’s the old version:

-- Splat
-- RJ 20200521

local Splats = {}

local Vecs = {
vec2(-2,0), vec2(-2,-2), vec2(2,-2), vec2(3,1), vec2(2,-1), vec2(0,2), vec2(1,3), vec2(-1,3), vec2(-4,-1), vec2(-3,1)
}

function drawSplats()
    for k, splat in pairs(Splats) do
        splat:draw()
    end
end

Splat = class()

function Splat:init(pos)
    local die = function()
        Splats[self] = nil
    end
    self.pos = pos
    Splats[self] = self
    self.size = 2
    self.diameter = 6
    self.rot = math.random(0,359)
    tween(4, self, {size=10, diameter=1}, tween.easing.linear, die)
end

function Splat:draw()
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    fill(255)
    stroke(255)
    rotate(self.rot)
    local s = self.size
    for i,v in ipairs(Vecs) do
        ellipse(s*v.x, s*v.y, self.diameter)
    end
    popMatrix()
    popStyle()
end

And here’s the new:

-- Splat
-- RJ 20200521
-- moved to U.indestructibles 20200612

local Vecs = {
vec2(-2,0), vec2(-2,-2), vec2(2,-2), vec2(3,1), vec2(2,-1), vec2(0,2), vec2(1,3), vec2(-1,3), vec2(-4,-1), vec2(-3,1)
}

Splat = class()

function Splat:init(pos)
    local die = function()
        U:deleteIndestructible(self)
    end
    self.pos = pos
    U:addIndestructible(self)
    self.size = 2
    self.diameter = 6
    self.rot = math.random(0,359)
    tween(4, self, {size=10, diameter=1}, tween.easing.linear, die)
end

function Splat:draw()
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    fill(255)
    stroke(255)
    rotate(self.rot)
    local s = self.size
    for i,v in ipairs(Vecs) do
        ellipse(s*v.x, s*v.y, self.diameter)
    end
    popMatrix()
    popStyle()
end

function Splat:move()
end

I remembered to add the move. Let’s see what I forgot.

Well, I forgot to remove drawSplats from draw. I don’t think a test would have helped with that, fwiw.

Everything works fine, the spats show up just as before.

In removing the call to drawSplats, I noticed drawScore and wonder whether the score should become a drawn indestructible. We also have the display that shows how many ships you have to do as well. We may get more use out of this collection than I foresaw.

Now what about the Explosion? We’re here to improve it, remember, and this refactoring is more or less in aid of doing that. Explosion is quite simple, and its init looks like this:

function Explosion:init(ship)
    local f = function()
        U:deleteObject(self)
    end
    self.pos = ship.pos
    self.step = vec2(0,0)
    U:addObject(self)
    tween(4, self, {}, tween.easing.linear, f)
end

It seems clear that we can change those two calls to addObject and be good to go. And I’ll remove the collision logic from here as well.

-- Explosion
-- RJ modified 20200612 indestructible

Explosion = class()

function Explosion:init(ship)
    local f = function()
        U:deleteIndestructible(self)
    end
    self.pos = ship.pos
    self.step = vec2(0,0)
    U:addIndestructible(self)
    tween(4, self, {}, tween.easing.linear, f)
end

function Explosion:draw()
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    fontSize(30)
    text("BLAMMO", 0, 0)
    popMatrix()
    popStyle()
end

function Explosion:move()
end

I expect this to work … and it does. the Saucer even got in a lucky shot on me, so I got to see that BLAMMO as well as the other.

Now for the fun, at last. Sorry to keep you waiting. I hope you aren’t too excited, this isn’t quite like a pony for Christmas.

Explosion, finally …

In my Codea spacewar game, I had to build an explosion for the ships. In the spirit of reuse, I’m going to port that code into Asteroids.

In Spacewar, the code looks like this:

Explosion = class(Thing)

function Explosion:init(pos, color)
   self.pos = pos
   self.color = color
   self.life = 10
   for i = 1,5 do
      local f = Fragment(self.pos, self.color, i==1)
      U:addObject(f)
   end
end

function Explosion:draw()
   self.life = self.life - DeltaTime
   if self.life <= 0 then
      U.GameOver = true
   end
end

Fragment = class(Thing)

function Fragment:init(pos, color, guy)
   self.pos = pos
   self.color = color
   self.frag = not guy
   self.base = vec2(0,0)
   self.ang= math.random(360)
   self.step = vec2(math.random(3),0):rotate(self.ang)
   self.spin = math.random(9)
   self.ds = 8*math.random()
   self.life = 4
end

function Fragment:move()
   self.pos = self.pos + self.step
   self.pos.x = self:range(0,self.pos.x,WIDTH)
   self.pos.y = self:range(0,self.pos.y,HEIGHT)
   self.spin = self.spin + self.ds
end

function Fragment:draw()
   self.life = self.life - DeltaTime
   if self.life < 0 then
      U:removeObject(self)
   end
   stroke(self.color)
   strokeWidth(2)
   noFill()
   translate(self.pos.x, self.pos.y)
   rotate(self.spin)
   if self.frag then
      line(-10,0,10,0)
      line(10,0,5,13)
   else
      scale(4,4)
      ellipse(0,5,8)
      line(0,3,0,-2)
      line(-4,2,4,2)
      line(0,-2,-3,-5)
      line(0,-2,3,-5)
   end
end

Now here, the Explosion has two purposes, it creates the Fragments, and then it times out and calls game over. We may indeed have that concern, but we do things a bit differently around here.

Splat works a bit differently, because although it looks like a bunch of dots moving independently, it’s really all just one pattern. Here, the fragments are independent, and they aren’t all alike. Let’s see how it goes. First, I’ll just import Fragment as it is into Asteroids, then fit it into our scheme.

I think this might do the job:


-- Fragment
-- RJ 20200612 from Spacewar

Fragment = class()

function Fragment:init(pos, guy)
    self.pos = pos
    self.color = 255
    self.frag = not guy
    self.base = vec2(0,0)
    self.ang= math.random(360)
    self.step = vec2(math.random(3),0):rotate(self.ang)
    self.spin = math.random(9)
    self.ds = 8*math.random()
    self.life = 4
    U:addIndestructible(self)
end

function Fragment:move()
   self.pos = self.pos + self.step
   self.pos.x = self.pos.x%WIDTH
   self.pos.y = self.pos.y%HEIGHT
   self.spin = self.spin + self.ds
end

function Fragment:draw()
   self.life = self.life - DeltaTime
   if self.life < 0 then
       U:deleteIndestructible(self)
   end
   stroke(self.color)
   strokeWidth(2)
   noFill()
   translate(self.pos.x, self.pos.y)
   rotate(self.spin)
   if self.frag then
       line(-10,0,10,0)
       line(10,0,5,13)
   else
       scale(4,4)
       ellipse(0,5,8)
       line(0,3,0,-2)
       line(-4,2,4,2)
       line(0,-2,-3,-5)
       line(0,-2,3,-5)
   end
end

And now I can change Explosion to create the Fragments and forget about them. It should go like this:

-- Explosion
-- RJ modified 20200612 indestructible
-- RJ modified 20200612 Fragments

Explosion = class()

function Explosion:init(ship)
    local pos = ship.pos
   for i = 1,5 do
       local f = Fragment(pos, i==1)
   end
end

After a little tweaking, I settle on this for Fragment:


-- Fragment
-- RJ 20200612 from Spacewar

Fragment = class()

function Fragment:init(pos, guy)
    self.pos = pos
    self.color = 255
    self.frag = not guy
    self.base = vec2(0,0)
    self.ang= math.random(360)
    self.step = vec2(2.0*math.random(),0):rotate(self.ang)
    self.spin = math.random(9)
    self.ds = 8*math.random()
    self.life = 4
    U:addIndestructible(self)
end

function Fragment:move()
    U:moveObject(self)
   self.spin = self.spin + self.ds
end

function Fragment:draw()
    self.life = self.life - DeltaTime
    if self.life < 0 then
        U:deleteIndestructible(self)
    end
    pushStyle()
    pushMatrix()
    stroke(self.color)
    strokeWidth(2)
    noFill()
    translate(self.pos.x, self.pos.y)
    rotate(self.spin)
    scale(1)
    if self.frag then
        line(-10,0,10,0)
        line(10,0,5,13)
    else
        scale(3)
        strokeWidth(1)
        ellipse(0,5,8)
        line(0,3,0,-2)
        line(-4,2,4,2)
        line(0,-2,-3,-5)
        line(0,-2,3,-5)
    end
    popMatrix()
    popStyle()
end

The Explosion class instances are now just created for long enough to create the fragments. I’m tempted to fold the two together, but for now, I’ll let it be. It might come in handy and it’s doing no harm.

I’ll close with a photo and a video of the explosion, so you can see why I like it well enough to reuse it.

Commit: “new explosion, separate drawing for indestructibles”.

explosion with little guy

explosion with spinning guy

Zipped Code