Spacewar! 10 - Who's got the button?
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