After a brief discussion, Tozier and I have decided that we don’t care which way the Button works. Our “reasoning” is that the Button has to recognize that it is or is not presently touched, but it also has to hold on to the touch.id, because when you put your finger down and just hold it, only one touch event occurs. So the Button will have to remember that it is touched, whether it actively sends messages or waits to be asked.

Now therefore, as Aquinas used to say, we’ll just push forward building a Button smart enough to do the job, and then a whole rack of them. We’re starting with button working like this:

function Button:touched(touch)
    if not self:insideCircle(touch.x,touch.y) then return end
    if touch.state == BEGAN or touch.state == MOVING then
        self.ship:perform(self.action)
    end
end

And the ship looks like this:

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:perform(functionName)
    return self[functionName](self)
end

function Ship:accelerate()
    self.accel = vec2(0,0.2)
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)
    self.accel = vec2(0,0)
end

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

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

In Main, we create the button with the text string “accelerate”, like this:

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
    button1accel = Button(ship1, WIDTH-100, HEIGHT/5, "accelerate")
end

(So far, we just have the one button, talking to the one ship.) Let’s look again at the working part:

function Button:touched(touch)
    if not self:insideCircle(touch.x,touch.y) then return end
    if touch.state == BEGAN or touch.state == MOVING then
        self.ship:perform(self.action)
    end
end

As it stands, we get an accelerate call at the moment we touch inside the circle, or when we move our finger about inside the circle, and it doesn’t matter which finger. This won’t do. Once the pilot has touched the button, we want to accelerate until he lifts his finger, or moves out of the circle. The issue is this: the touched event only occurs when the button is touched by a finger (moving or not) in that moment. The button does not execute in every draw cycle: it’s asynchronous. One solution (the only one I can think of) is that the button must detect the UP/DOWN whenever it happens, and send accelerate(true) on the one and accelerate(false) on the other. (If the ship were asking, instead of being told, the button would only need to remember state. But if it’s in control, it has to act every time its pressed / not pressed state changes. If we were asking, we’d just call from the ship to the button during the draw cycle. So our question is, do we go forward with the button being active, or make it passive? We decide: passive.

OK. So now “all” we have to do is make our button know whether it is pressed or not pressed. Here goes …

function Button:touched(touch)
    if ( self.capturedID == touch.id ) then
        if ( touch.state == ENDED ) then
            self.pressed = false
            self.capturedID = nil
        else -- MOVING
            if ( self:insideCircle(touch.x,touch.y) ) then
                self.pressed = true
            else -- not inside
                self.pressed = false
            end
        end
    elseif ( self.capturedID == nil ) then
        if ( touch.state == BEGAN and self:insideCircle(touch.x, touch.y) ) then
            self.pressed = true
            self.capturedID = touch.id
        end
    end
end

OK, this is hideous. We created it a bit at a time, thinking carefully about all the random cases we could imagine. It seems to work, and I, for one, hate it. (Tozier: “Seconded”)

I suggest reversing the order of the outer if, putting the capturedID == nil case first, it’d be more clear. Tozier agrees, pointing out that that’s the first case that happens and putting it first helps. But it doesn’t make it much less messy.

Perhaps there is a way to refactor that mess. (Exercise left to the reader.) Instead we decided to write it “by intention”, that is, to write down what we intend to have happen, and then make it happen. Thus:

function Button:touched(touch) 
    self.capturedID = self:getCapture(touch)
    self.pressed = self:getPressed(touch)
end

We want to get the capturedID, and we want to get the pressed state of the button. (As I write this, I don’t like that last name, but that’s for later.)

The button captures an ID if it doesn’t have one and the touch is inside the circle and the state is BEGAN. It drops its captured ID when the captured ID’s state is ENDED. Otherwise its captured ID is whatever it already is. Thus:

function Button:getCapture(touch) 
    if touch.state == BEGAN and self:insideCircle(touch.x, touch.y) and not self.capturedID then
        return touch.id
    elseif touch.state == ENDED and self.capturedID == touch.id then
        return nil
    else
        return self.capturedID
    end
end

However, we didn’t just type this in. In particular we got in a lot of trouble with the and, which short-circuits in Lua. Note my phrase above “if it doesn’t have one and the touch is inside and the state is BEGAN”. That phrase, in that order, will capture any touch if we don’t have one. The correct idea is as in the code, namely if the state is BEGAN and it’s inside our circle and we don’t already have a captured ID. This had us chasing our tail for quite a while. And we don’t even have tails. Anyway this code seems to work correctly, and here’s the press logic:

If we don’t have a captured ID, we’re not pressed. Otherwise, if the touch is inside us and is the one we’ve captured, we’re pressed. Thus:

function Button:getPressed(touch)
    if not self.capturedID then
        return false
    else 
        return self:insideCircle(touch.x,touch.y) and self.capturedID == touch.id
    end
end

We tested all this by turning our button red when it’s not pressed, green when it is, and it looks like this. FIX THIS TO SHOW FINGER, THEN RECORD MOVIE.

NOTES FOR TOMORROW?

Show finger Make ship move; Max speed; Stay alert for flickering To what extent are these difficulties due to not having unit tests? MOVIE HERE