Spacewar! 06 - Giving these ships some real class.
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:
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.
-
Brighton Agile Roundtable ↩