Can’t have a Space War with only one ship

Spacewar needs two ships. We’re inclined to do a bit of that next, to see what we can learn. Neither you nor we would really do this, but sometimes people cut and paste when they need to do something like this. Let’s try that to see what it looks like.

We have variables like pos, vel, heading. We could rename those to pos1, and so on and then use pos2 for the second ship. I think we’ll skip the first part, because people often do, and just use pos2 and so on anyway.

We expect to encounter some duplication while doing this. As faithful readers know, duplication is the enemy. However, if we let it occur and then fix it, it will teach us about the true shape of the problem. If we try to guess what to do, we’re likely to do too much and still miss things. Let’s see what happens.

Here’s our 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
end

And in the draw loop:

    -- draw
    strokeWidth(2)
    translate(pos:unpack())
    rotate(heading)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    
    translate(pos2:unpack())
    rotate(heading2)
    ellipse(0,0,10,15)
    line(0,0,0,10)

This doesn’t work! It looks like this:

MOVIE HERE

So. It turns out that translate() and rotate() are cumulative. Each one adds to the previous. What are we going to do about this? Yes, well, Codea Lua has just the thing. The function pushMatrix() saves the current settings of the graphical transform matrix, and popMatrix() restores that saved setting. Whenever we draw anything that fiddles the matrix, we should push and pop. Similarly for style (fill, line width, and so on) by the way, but we’re not ready for that. So our new code is:

    -- draw
    strokeWidth(2)
    pushMatrix()
    translate(pos:unpack())
    rotate(heading)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()
    
    pushMatrix()
    translate(pos2:unpack())
    rotate(heading2)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()

And it looks like this now. Note that our right-hand ship drives about, and the left is no longer attached to it with a long invisible stick. Lovely. (Personal communication: T.)

MOVIE HERE

As we look at the code now, we see enough awful things so that even we, in our addled state, see the need for improvement. Some issues are:

  • Duplication of ship drawing. We can also see that if we wanted to move our second ship, we’d have to duplicate that code.
  • The strokeWidth is probably a ship property and should probably be inside the push/pop.
  • The draw function includes a number of separate notions: it defines and reads controls; it moves the ship(s); it draws the ships.

Let’s begin by removing the duplication in the drawing. This calls for the pattern Extract Function (like Extract Method, except these aren’t objects yet. A glance at the code tells us that drawing the ship uses its heading, position, and velocity. (We foresee a concern about updating but that’s not drawing. We’ll leave it for later. One thing at a time, always.

    -- draw
    strokeWidth(2)
    drawShip(pos, heading)
    
    pushMatrix()
    translate(pos2:unpack())
    rotate(heading2)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()
end

function drawShip(shipPos, shipHeading)
    pushMatrix()
    translate(pos:unpack())
    rotate(heading)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()
end

Note that we extracted the code from the first ship drawing and used it to create the function. We were smart enough to rename the variables. Had we not, pos and heading would have referred to the globals unless we put them in as arguments. A quick test shows us that the system still works. We’ll change the drawing of the second ship similarly. But wait? How will we know that it actually works? The second ship doesn’t move or anything. We’ll worry about that in a moment: we’re on a roll just now.

So this totally doesn’t work. Duh: we gave the draw function those nice parameters and didn’t use them inside. Did you notice that? If so, why didn’t you mention it?

function drawShip(shipPos, shipHeading)
    pushMatrix()
    translate(shipPos:unpack())
    rotate(shipHeading)
    ellipse(0,0,10,15)
    line(0,0,0,10)
    popMatrix()
end

That’s better. We’re talking about what to do next. Look at the entire draw() function:

function draw()
    background(40, 40, 50)
    
    -- controls
    Turn = 0
    local accel = vec2(0,0)
    if ( CurrentTouch.state == BEGAN or CurrentTouch.state == MOVING ) then
        if (CurrentTouch.x < WIDTH/3) then 
            Turn = 1
        end
        if ( CurrentTouch.x > WIDTH*0.66) then
            Turn = -1
        end
        if ( CurrentTouch.x >= WIDTH/3 and CurrentTouch.x <= WIDTH*0.66 ) then
            accel = vec2(0, 0.02)
        end
    end
    
    -- move
    heading = adjustedHeading(heading, Turn)
    vel = adjustedVelocity(vel, heading, accel)
    pos = clip_to_screen(pos + vel)
    
    -- draw
    strokeWidth(2)
    drawShip(pos, heading)
    drawShip(pos2, heading2)

end

We observe one trivial matter: we should move the setting of strokeWidth inside the draw: it’s a property of what the ship looks like. We’ll do that shortly.

The code transformation we just did, Extract Function, is trying to apply what we might call the Composed Function pattern, namely that any function should either do things, or call other functions, but not both. (cf Composed Method.) We see two things needing extraction, the controls, and the move. Suppose we pick move, thinking it’s easier because we don’t even have control events for the second ship. The move function will want to adjust heading, vel, and pos for the first ship and heading2 and so on for the second. How could it do that??? We don’t have pointer variables in Codea: we can’t pass in a variable and get it changed outside.

Each ship wants to have its own position, heading, velocity, acceleration, and so on. What mechanism have we to do this, class? (Class exclaims: “Objects, sir!”) Good job, class. We want a class .. .Ship. (Sorry.) Much as we might like to build in more function, much as we might like to clean up the code in place, the code is telling us otherwise. The code is saying it’s time to create a Ship class. We can but obey.

Tune in next time, and we’ll do just that thing.