Dungeon 211 - What's a Petard?
I believe that I have fallen into my own trap, Could this be the end of our intrepid hero? Click on in and see.
Faithful readers will recall that we’re in the process of arranging our buttons and inventory items so that two things happen:
- You can touch and then drag your finger away, resulting in no action on a button, and a display of information for the inventory;
- The button or item highlights when touched, so that our little movies make it more clear what the operator is doing.
Last time, we implemented the ability for a button or item to “capture” the touch, directing all further touch info to a function provided, which allowed us to do the drag-away behavior:
function Button:touched(aTouch, player)
if aTouch.state == BEGAN and self:mine(aTouch) then
Runner:capture(function(aTouch)
self:capturedTouched(aTouch, player)
end)
end
end
In GameRunner, we just save away that function and when a touch event occurs, we execute the function. One of the possible functions is the one that handles a touch when nothing is as yet captured:
function GameRunner:capture(aFunction)
self.touchHandler = aFunction or function(aTouch)
self:basicTouched(aTouch)
end
end
function GameRunner:basicTouched(aTouch)
for i,b in ipairs(self.buttons) do
b:touched(aTouch, self.player)
end
Inventory:touched(aTouch)
end
Now we want to make the buttons and items highlight. That needs to do something like this for buttons:
function Button:draw()
pushMatrix()
pushStyle()
spriteMode(CENTER)
translate(self.x,self.y)
if this button is captured then
tint(255,255,255, 255)
else
tint(255,255,255, 128)
end
sprite(Sprites:sprite(self.img), 0,0, self.w, self.h)
popStyle()
popMatrix()
end
When I apply that different tint to one button, the effect is just about right:
The question facing me is how I can possibly know that the button we’re currently drawing is the captured one. The capture information is just a function that we call on touch.
In yesterday’s article, when considering how to do the capture, I mentioned the function approach that I finally used, or an alternative of putting a small object into the capture variable. I chose the function mostly to see how it would go. It’s a fairly “Lua” thing to do, but not as object-oriented as I usually choose to do things.
It looks now that that wasn’t the best idea. No problem, we’ll do something different. But what?
At present, there is no real record of what button or item is captured. The function saved by the capture knows self
, and it does what has to be done. We need to do better.
Imagine a very small “capture” object, containing a button or inventory item, and whatever other information we need in order to work, with two methods: touched
and highlight
(or maybe shouldHighlight
). We’d have three versions of that object just as we now have three versions of the capture functions, one for normal, one for button, one for inventory item.
The main touched
method would call touched
on the object and … hmm … how would the draw know how to access the information? We could certainly reach back up to Runner, but that’s not as cool as it might be.
Let’s review both draw methods:
function Button:draw()
pushMatrix()
pushStyle()
spriteMode(CENTER)
translate(self.x,self.y)
if self.name == "learn" then
tint(255,255,255,255)
else
tint(255,255,255, 128)
end
sprite(Sprites:sprite(self.img), 0,0, self.w, self.h)
popStyle()
popMatrix()
end
(I’ve left my color-testing if in there.)
function InventoryItem:draw(pos)
fill(136,129,107)
rect(pos.x,pos.y,64,64)
sprite(Sprites:sprite(self.icon), pos.x,pos.y, 40)
end
Very similar and whatever works for one should work for both. But what do we want to say or do?
The caller of these methods is GameRunner for buttons, and Inventory for inventory items:
function GameRunner:drawButtons()
for i,b in ipairs(self.buttons) do
b:draw()
end
end
function Inventory:draw()
pushMatrix()
pushStyle()
rectMode(CENTER)
spriteMode(CENTER)
textMode(CENTER)
for i,entry in ipairs(self:entries()) do
local pos = self:drawingPos(i, self:count())
entry.item:draw(pos)
if entry.count > 1 then
fill(255)
fontSize(36)
text(entry.count, pos.x+20,pos.y-15)
end
end
popStyle()
popMatrix()
end
At this moment, I have no good idea, but I do have an idea that would probably work.
If–and hear me out–if, at the same time we call capture
, we were to store a flag in the captured object, we could use the flag to do the highlight. But we’d have to remember to clear the flag or pretty soon everything would be highlighted.
We’d have coupling, between the capture function and the object whose flag was set.
There’s no question that we could make this work, and we wouldn’t have to change the capture functions. But coupling should be represented, not in two lines of code, in two different places, that have to be inverses of each other, but by one thing, ideally in one place. (We are forced to have two, one to set and one to clear, by the nature of touch events. And in each capturing object as well. Definitely a tiny bit of duplication there that we’d do better to avoid. Not now: we’re on highlight.
I say we try it and see how bad it is.
For Button:
function Button:draw()
pushMatrix()
pushStyle()
spriteMode(CENTER)
translate(self.x,self.y)
if self.highlight then
tint(255,255,255,255)
else
tint(255,255,255, 128)
end
sprite(Sprites:sprite(self.img), 0,0, self.w, self.h)
popStyle()
popMatrix()
end
function Button:capturedTouched(aTouch, player)
if aTouch.state == ENDED and player:itsOurTurn() then
if self:mine(aTouch) then
self:performCommand(player)
else
-- lifted outside
end
Runner:capture(nil)
self.highlight = false
end
end
function Button:touched(aTouch, player)
if aTouch.state == BEGAN and self:mine(aTouch) then
Runner:capture(function(aTouch)
self:capturedTouched(aTouch, player)
end)
self.highlight = true
end
end
I think this probably works. And it does.
Easy to replicate that in InventoryItem:
function InventoryItem:draw(pos)
fill(136,129,107)
rect(pos.x,pos.y,64,64)
if self.highlight then
tint(255,255,255,255)
else
tint(255,255,255, 128)
end
sprite(Sprites:sprite(self.icon), pos.x,pos.y, 40)
end
function InventoryItem:touched(aTouch, pos)
if aTouch.state == BEGAN and manhattan(aTouch.pos,pos) < ItemWidth//2 then
Runner:capture(function(aTouch)
self:capturedTouched(aTouch,pos)
end)
self.highlight = true
end
end
function InventoryItem:capturedTouched(aTouch, pos)
if aTouch.state == ENDED then
if manhattan(aTouch.pos, pos) < ItemWidth//2 then
Inventory:remove(self)
self:informObjectRemoved()
self:usedMessage()
Bus:publish(self.attribute, self.value1, self.value2)
else
Runner:capture(nil)
self.highlight = false
self:informObjectTouched()
end
end
end
I don’t like the effect of the tint on the more beige inventory items. I’ll fiddle with the values a bit.
I can’t find a color that works well. Let’s try something different.
OK, I found a setup that looks good:
function InventoryItem:draw(pos)
if self.highlight then
fill(255,255,255,64)
else
fill(255,255,255, 192)
end
rect(pos.x,pos.y,64,64)
if self.highlight then
tint(255,255,255,64)
else
tint(255,255,255, 255)
end
sprite(Sprites:sprite(self.icon), pos.x,pos.y, 40)
end
I’ll look at reducing the two if statements in a moment but I’ve found a bug.
In the movie above you can see that i touch and drag on the Rock of Dullness. What you can’t see is that then I tried to use the pathfinder, and got a spurious message about touching the rock of dullness.
That tells me that the item hasn’t cleared the capture in the case of touch-drag. Can that be true?
function InventoryItem:capturedTouched(aTouch, pos)
if aTouch.state == ENDED then
if manhattan(aTouch.pos, pos) < ItemWidth//2 then
Inventory:remove(self)
self:informObjectRemoved()
self:usedMessage()
Bus:publish(self.attribute, self.value1, self.value2)
else
Runner:capture(nil)
self.highlight = false
self:informObjectTouched()
end
end
end
Sure is. We need to undo the capture whenever the state is ENDED. I think that whole else
clause needs to be moved after the manhattan check and made unconditional.
I kind of hope this breaks a test but I suspect it won’t.
function InventoryItem:capturedTouched(aTouch, pos)
if aTouch.state == ENDED then
if manhattan(aTouch.pos, pos) < ItemWidth//2 then
Inventory:remove(self)
self:informObjectRemoved()
self:usedMessage()
Bus:publish(self.attribute, self.value1, self.value2)
end
Runner:capture(nil)
self.highlight = false
self:informObjectTouched()
end
end
I’ll try it again. The touched message came out on use and shouldn’t have. Moved too much.
function InventoryItem:capturedTouched(aTouch, pos)
if aTouch.state == ENDED then
if manhattan(aTouch.pos, pos) < ItemWidth//2 then
Inventory:remove(self)
self:informObjectRemoved()
self:usedMessage()
Bus:publish(self.attribute, self.value1, self.value2)
else
self:informObjectTouched()
end
Runner:capture(nil)
self.highlight = false
end
end
OK, that did the trick. I’m going to commit this: button and inventory highlight work.
Now I’ll try combining the two ifs. It should be OK graphically.
function InventoryItem:draw(pos)
if self.highlight then
fill(255,255,255,64)
tint(255,255,255,64)
else
fill(255,255,255, 192)
tint(255,255,255, 255)
end
rect(pos.x,pos.y,64,64)
sprite(Sprites:sprite(self.icon), pos.x,pos.y, 40)
end
Works fine. Commit: refactor for just one if-else in inventory item draw.
I need to finish up. Errands and stuff this morning. Let’s sum up:
Summary
Well, it works, and we didn’t have to wreck our capture function code (or trick, if you want to call it that).
I’d be happier with something like this:
function InventoryItem:draw(pos)
pushStyle()
self:setColoration()
rect(pos.x,pos.y,64,64)
sprite(Sprites:sprite(self.icon), pos.x,pos.y, 40)
popStyle()
end
function InventoryItem:setColoration()
if self.highlight then
fill(255,255,255,64)
tint(255,255,255,64)
else
fill(255,255,255, 192)
tint(255,255,255, 255)
end
end
Im still troubled because we have to do two things on capture, one being to capture the touch behavior up at the very top, and the other being to highlight the individual button or item. And then we have to undo both. And I’ve demonstrated that it’s possible to get it wrong.
It seems intuitively that these things are the same. They’re certainly temporally coupled. But the one is so high level, and the other so low, that it makes some sense to allow them to be separate though coupled.
At the moment, I’m not satisfied, but everything is working as intended, so we can ship it. Maybe I’ll get an idea. Maybe you, or someone, will send me one.
See you next time!