Giving these ships some class

Tozier has arrived here at the BAR1 and our plan is to move our strange two-ship code into one or more Lua classes. But first, some comments.

It may seem abysmally stupid to you for us to have written the second ship in that strange ad-hoc fashion. Wasn’t it obvious that we needed a Ship class already? Wasn’t it even obvious before we wrote the in-line code for the first one? Well, maybe it was.

However, I’ve seen a lot of strange code over the years, and lots of it was this year and last year, not just back in the era of the Big Bopper. And I’ve written some as well. I’ve come full circle, from probably writing bad code, to TDDing everything and making classes of everything, to writing more and more experimental code in line and then refactoring. I do that for at least two reasons:

  • practice recovering bad code
  • learning what I mean before I format it

We begin like this:

Ship = class()

function Ship:init(pos, heading)
    self.pos = pos
    self.heading = heading
end

function Ship:draw()
    
end

We have been a bit presumptuous in assuming that a Ship has a position and a heading (hmm, should we spell out “position”?) but we’ve been working on this a while and we know darn well that we need those. I bet we’re not even wrong. We expect to have to draw the ships, so we have an empty draw method. It remains to create one or two ships and draw them, then begin to move them about and so on.

Tozier suggests making a third ship, which seems interesting. We’ll do that. Hold my beer.

Here’s what we added to Main setup:

function setup()
    local offset = 200
    pos = vec2(WIDTH/2 + offset, HEIGHT/2)
    vel = vec2(0,0)
    heading = 0
    pos2 = vec2(WIDTH/2 - offset, HEIGHT/2)
    vel2 = vec2(0,0)
    heading2 = 180
    
    ship3 = Ship(vec2(WIDTH/2, HEIGHT/2), 90)
end

And in draw we added just one line:

    -- draw
    strokeWidth(2)
    drawShip(pos, heading)
    drawShip(pos2, heading2)
    
    ship3:draw()

And here’s the ship:

Ship = class()

function Ship:init(pos, heading)
    self.pos = pos
    self.heading = heading
end

function Ship:draw()
    pushMatrix()
    translate(self.pos:unpack())
    rotate(self.heading)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()
end

This draws our third ship as you’d expect:

Three ships

We consider this to be nigh on to miraculous. We don’t really need three ships, but now we can confidently draw the two that we do need. Forgive me if I call them Ship1 and Ship2 this time.

function draw()
    background(40, 40, 50)
    
    ship1:control()
    ship2:control()
    
    ship1:move()
    ship2:move()
    
    -- draw
    strokeWidth(2)
    ship1:draw()
    ship2:draw()
end

And in Ship:

Ship = class()

function Ship:init(pos, heading)
    self.pos = pos
    self.heading = heading
end

function Ship:control()
    
end

function Ship:move()
    
end

function Ship:draw()
    pushMatrix()
    translate(self.pos:unpack())
    rotate(self.heading)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()
end

We actually did this in two steps: we put in the calls to draw, while removing all the old ship code, then we added the calls to control() and move(), noting that we had those phases in the original version. Right now they do nothing.

I’d like to move over the move code from the first version, and ignore the controls for now. We’ll clean up the names a bit. “Turn”, in particular, bugs me. Tozier accepts this plan with a slight look of sadness.

Here’s Ship class with move logic. We just copied all this over and recast all the variables with self. and initialized the new variables in setup. (Reference to an unset variable upsets Lua.)

Ship = class()

function Ship:init(pos, heading)
    self.pos = pos
    self.heading = heading
    self.vel = vec2(0,0)
    self.turn = 0
    self.accel = vec2(0,0)
end

function Ship:control()
    
end

function Ship:move()
    self.heading = adjustedHeading(self.heading, self.turn)
    self.vel = adjustedVelocity(self.vel, self.heading, self.accel)
    self.pos = clip_to_screen(self.pos + self.vel)
end

function Ship:draw()
    pushMatrix()
    translate(self.pos:unpack())
    rotate(self.heading)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()
end

function clip_to_screen(vec)
    return vec2(vec.x%WIDTH, vec.y%HEIGHT)
end

function adjustedHeading(heading, turn)
    return heading + turn
end

function adjustedVelocity(vel, heading, accel)
    return vel + accel:rotate(math.rad(heading))
end

This all works as advertised, i.e. does nothing. There are no controls, and the ships do not turn or move. I plan to jam some values into them to make them move, just to be sure this works.

We change the Main setup to give the ships some fixed turn rates and acceleration:

function setup()
    local offset = 200
    ship1 = Ship(vec2(WIDTH/2 + offset, HEIGHT/2), 0)
    ship2 = Ship(vec2(WIDTH/2 - offset, HEIGHT/2), 180)
    
    ship1.accel = vec2(0,0.02)
    ship1.turn = 0.5
    ship2.accel = vec2(0,0.01)
    ship2.turn = -0.4
end

This works delightfully! As you watch this, please imagine your favorite Strauss waltz playing.

MOVIE

So we are confident that motion works as well as it ever did. And we have controls left to do. Rather than move over our hack touch left of screen to go left, I think we should work out how to do real controls, whatever that means when we figure it out. I can’t wait to find out. That’s best left for another day.

However, we notice this little issue:

    -- draw
    strokeWidth(2)
    ship1:draw()
    ship2:draw()

The stroke width should be a property of the thing drawn, so it should move from here into the draw() method of the Ship. And strokeWidth is a graphics “style” so we push and pop the style, viz:

function Ship:draw()
    pushStyle()
    strokeWidth(2)
    pushMatrix()
    translate(self.pos:unpack())
    rotate(self.heading)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()
    popStyle()
end

We could put other things in style, such as ship color, fill settings, etc. But for now, it’s time to break.

  1. Brighton Agile Roundtable